1   package com.trendmicro.grid.acl.ds.jpa.util;
2   
3   import com.trendmicro.grid.acl.commons.SoftReferenceThreadLocal;
4   import com.trendmicro.grid.acl.l0.datatypes.AbstractListPage;
5   import com.trendmicro.grid.acl.l0.datatypes.MetadataOwner;
6   import com.trendmicro.grid.acl.l0.datatypes.NameListPage;
7   import com.trendmicro.grid.acl.l0.datatypes.Tagged;
8   import com.trendmicro.grid.acl.metadata.Metadata;
9   import com.trendmicro.grid.acl.metadata.ValidationContext;
10  
11  import javax.persistence.NonUniqueResultException;
12  import javax.persistence.Query;
13  import javax.persistence.TypedQuery;
14  import javax.xml.bind.JAXBException;
15  import javax.xml.bind.Marshaller;
16  import javax.xml.bind.PropertyException;
17  import javax.xml.bind.Unmarshaller;
18  import javax.xml.transform.Source;
19  import javax.xml.transform.stax.StAXSource;
20  import javax.xml.transform.stream.StreamSource;
21  import java.io.StringReader;
22  import java.io.StringWriter;
23  import java.nio.ByteBuffer;
24  import java.util.*;
25  import java.util.concurrent.Callable;
26  
27  import static com.trendmicro.grid.acl.Limits.SERIALIZED_METADATA_LENGTH;
28  import static com.trendmicro.grid.acl.Limits.TAG_STRING_LENGTH;
29  import static net.sf.tinyjee.util.Assert.assertEquals;
30  
31  /**
32   * A small set of common helper methods used to simplify working with JPA.
33   *
34   * @author juergen_kellerer, 2010-06-07
35   * @version 1.0
36   */
37  public class JpaUtils {
38  
39  	//Don't remove the line below!
40  	// START SNIPPET: StorageOptions
41  
42  	/**
43  	 * Toggles whether EMPTY metadata and 'NULL' are treated equal. (defaults to 'true')
44  	 * <p/>
45  	 * When 'true' the ACL normalizes empty metadata to 'null' to reduce the storage
46  	 * overhead inside the database.
47  	 */
48  	public static final boolean EMPTY_METADATA_EQUALS_NULL = Boolean.parseBoolean(
49  			System.getProperty("gacl.empty.metadata.equals.null", Boolean.TRUE.toString()));
50  
51  	//END SNIPPET: StorageOptions
52  	//Don't remove the line above!
53  
54  	private static final TagsSerializedCache TAGS_SERIALIZED_CACHE = new TagsSerializedCache();
55  
56  	/**
57  	 * Creation of Unmarshallers has shown to be expensive, caching them in a TL.
58  	 */
59  	private static final SoftReferenceThreadLocal<Unmarshaller> META_UNMARSHALLERS =
60  			new SoftReferenceThreadLocal<Unmarshaller>() {
61  				@Override
62  				protected Unmarshaller createInitialValue() throws Exception {
63  					return Metadata.getXmlSerializer().getJaxbContext().createUnmarshaller();
64  				}
65  			};
66  
67  	/**
68  	 * Creation of Marshallers has shown to be expensive, caching them in a TL.
69  	 */
70  	private static final SoftReferenceThreadLocal<Marshaller> META_MARSHALLERS = new SoftReferenceThreadLocal<Marshaller>() {
71  		@Override
72  		protected Marshaller createInitialValue() throws Exception {
73  			Marshaller marshaller = Metadata.getXmlSerializer().getJaxbContext().createMarshaller();
74  			try {
75  				marshaller.setProperty("com.sun.xml.bind.xmlDeclaration", Boolean.FALSE);
76  			} catch (PropertyException ignored) {
77  				marshaller.setProperty("com.sun.xml.internal.bind.xmlDeclaration", Boolean.FALSE);
78  			}
79  			return marshaller;
80  		}
81  	};
82  
83  	/**
84  	 * Defines a callback to use for converting types.
85  	 *
86  	 * @param <E> The input type.
87  	 * @param <V> The converted output type.
88  	 */
89  	public interface Callback<E, V> {
90  		V call(E element);
91  	}
92  
93  	/**
94  	 * Straight converter callback that simply passed single or array input to a single output.
95  	 */
96  	public static final Callback SINGLE_ELEMENT_CALLBACK = new Callback<Object, Object>() {
97  		@Override
98  		public Object call(Object element) {
99  			if (element instanceof Object[]) {
100 				Object[] array = (Object[]) element;
101 				return array[array.length - 1];
102 			}
103 			return element;
104 		}
105 	};
106 
107 	/**
108 	 * Encodes a UUID to its byte representation.
109 	 *
110 	 * @param guid the UUID to convert.
111 	 * @return the encoded UUID bytes.
112 	 */
113 	public static byte[] toBytes(UUID guid) {
114 		if (guid == null)
115 			return null;
116 		return ByteBuffer.allocate(16).putLong(guid.getMostSignificantBits()).putLong(guid.getLeastSignificantBits()).array();
117 	}
118 
119 	/**
120 	 * Decodes a UUID from its byte representation.
121 	 *
122 	 * @param publicGUID the UUID to convert.
123 	 * @return the decoded UUID.
124 	 */
125 	public static UUID fromBytes(byte[] publicGUID) {
126 		if (publicGUID == null)
127 			return null;
128 
129 		assertEquals("publicGUID#length", 16, publicGUID.length);
130 
131 		ByteBuffer buffer = ByteBuffer.wrap(publicGUID);
132 		return new UUID(buffer.getLong(), buffer.getLong());
133 	}
134 
135 	/**
136 	 * Serializes the given tag array to text.
137 	 *
138 	 * @param tags the tags to encode.
139 	 * @return the text encoded tags.
140 	 */
141 	public static String serializeTags(String[] tags) {
142 		if (tags == null || tags.length == 0) return null;
143 
144 		// Lots of elements are tagged with the same tags.
145 		// Caching the serialization results speeds-up the storage.
146 		String result = TAGS_SERIALIZED_CACHE.get(tags);
147 
148 		if (result == null) {
149 			// Clone the tags to avoid modifying external values.
150 			tags = tags.clone();
151 
152 			final Locale locale = Locale.getDefault();
153 			int len = 1;
154 			for (int i = 0; i < tags.length; i++) {
155 				final String tag = tags[i];
156 				len += tag.length() + 1;
157 				tags[i] = tag.replace(' ', '_').toLowerCase(locale);
158 			}
159 
160 			result = TAGS_SERIALIZED_CACHE.get(tags);
161 
162 			if (result == null) {
163 
164 				Arrays.sort(tags);
165 
166 				final StringBuilder builder = new StringBuilder(len).append(' ');
167 				for (String tag : tags)
168 					builder.append(tag).append(' ');
169 
170 				result = builder.toString();
171 				if (result.length() > TAG_STRING_LENGTH) {
172 					throw new IllegalArgumentException("The tag list '" + result + "' exceeds the maximum length of '" +
173 							TAG_STRING_LENGTH + "' characters.");
174 				}
175 
176 				TAGS_SERIALIZED_CACHE.put(tags, result);
177 			}
178 		}
179 
180 		return result;
181 	}
182 
183 	/**
184 	 * Deserizalizes the given serialized tags.
185 	 *
186 	 * @param tags the text encoded tags.
187 	 * @return the deserialized tag array.
188 	 */
189 	public static String[] deserializeTags(String tags) {
190 		if (tags == null || tags.isEmpty()) return null;
191 
192 		String[] result = TAGS_SERIALIZED_CACHE.getReverse(tags);
193 
194 		if (result == null) {
195 			List<String> tagList = new ArrayList<String>(5 + (tags.length() / 8)); // assuming avg tag size is 8 chars.
196 
197 			final Locale locale = Locale.getDefault();
198 			final StringTokenizer tok = new StringTokenizer(tags, " ");
199 			while (tok.hasMoreTokens()) {
200 				tagList.add(tok.nextToken().toLowerCase(locale));
201 			}
202 
203 			result = tagList.toArray(new String[tagList.size()]);
204 
205 			// For security reasons tags are sorted again when read.
206 			// (to avoid that external DB modifications break the ACL internal logic when dealing with tags)
207 			Arrays.sort(result);
208 
209 			TAGS_SERIALIZED_CACHE.put(tags, result);
210 		}
211 
212 		// Clone the result before returning to avoid that external implementation can modify the cached values.
213 		return result.clone();
214 	}
215 
216 	/**
217 	 * Serializes the given metadata to XML.
218 	 *
219 	 * @param metadata the metadata to encode.
220 	 * @return the XML encoded metadata.
221 	 */
222 	public static String serializeMetadata(Metadata metadata) {
223 		if (metadata == null || (EMPTY_METADATA_EQUALS_NULL && metadata.getMetaElements().isEmpty())) return null;
224 
225 		// Make sure all meta values are valid before allowing to write them.
226 		metadata.assertValuesAreValidForWrite();
227 
228 		final StringWriter w = new StringWriter(2048);
229 		try {
230 			META_MARSHALLERS.getValue().marshal(metadata, new LimitedWriter(w, SERIALIZED_METADATA_LENGTH));
231 			return w.toString();
232 		} catch (JAXBException e) {
233 			if (e.getCause() instanceof LimitedWriter.OutOfBounds) {
234 				throw new IllegalArgumentException("The metadata element exceeds the maximum length of '" +
235 						SERIALIZED_METADATA_LENGTH + "' characters.\n\n" + w.toString(), e);
236 			} else
237 				throw new RuntimeException(e);
238 		}
239 	}
240 
241 	/**
242 	 * Deserizalizes the given XML serialized metadata.
243 	 *
244 	 * @param rawData the XML encoded metadata.
245 	 * @return the deserialized metadata.
246 	 */
247 	public static Metadata deserializeMetadata(final String rawData) {
248 		if (rawData == null || rawData.isEmpty()) return null;
249 
250 		return deserializeMetadata(new StreamSource(new StringReader(rawData)));
251 	}
252 
253 	/**
254 	 * Deserizalizes the given XML serialized metadata.
255 	 *
256 	 * @param rawData the XML encoded metadata.
257 	 * @return the deserialized metadata.
258 	 */
259 	public static Metadata deserializeMetadata(final Source rawData) {
260 		if (rawData == null) return null;
261 
262 		try {
263 			return ValidationContext.getInstance().doUnvalidated(new Callable<Metadata>() {
264 				@Override
265 				public Metadata call() throws Exception {
266 					final Unmarshaller um = META_UNMARSHALLERS.getValue();
267 					final Metadata metadata;
268 					if (rawData instanceof StAXSource) {
269 						final StAXSource source = (StAXSource) rawData;
270 						metadata = source.getXMLEventReader() == null ?
271 								(Metadata) um.unmarshal(source.getXMLStreamReader()) :
272 								(Metadata) um.unmarshal(source.getXMLEventReader());
273 					} else
274 						metadata = (Metadata) um.unmarshal(rawData);
275 
276 					if (EMPTY_METADATA_EQUALS_NULL && metadata.getMetaElements().isEmpty())
277 						return null;
278 
279 					return metadata;
280 				}
281 			});
282 		} catch (Exception e) {
283 			throw new RuntimeException(e);
284 		}
285 	}
286 
287 	/**
288 	 * Returns true if the 2 metadata owning instances differ.
289 	 *
290 	 * @param a the first instance to check.
291 	 * @param b the second instance to check.
292 	 * @return true if the 2 metadata owning instances differ.
293 	 */
294 	public static boolean metadataDiffers(MetadataOwner a, MetadataOwner b) {
295 		final Metadata ma = a.getMetadata(), mb = b.getMetadata();
296 		if (ma == null)
297 			return mb != null;
298 		else
299 			return mb == null || !mb.equals(ma);
300 	}
301 
302 	/**
303 	 * Returns true if the 2 tagged instances differ.
304 	 *
305 	 * @param a the first instance to check.
306 	 * @param b the second instance to check.
307 	 * @return true if the 2 tagged instances differ.
308 	 */
309 	public static boolean tagsDiffer(Tagged a, Tagged b) {
310 		Collection<String> ta = a.getTags(), tb = b.getTags();
311 
312 		if (ta.size() != tb.size()) return true;
313 
314 		ta = new HashSet<String>(ta);
315 		for (String s : tb) {
316 			if (!ta.contains(s)) return true;
317 		}
318 
319 		return false;
320 	}
321 
322 	/**
323 	 * Returns true if the element is tagged with the given tags.
324 	 *
325 	 * @param element the element to check.
326 	 * @param tags    the tags to verify.
327 	 * @return true if the element contains all the tags, false if not, 'null' if the element was 'null'.
328 	 */
329 	public static Boolean isTaggedWith(Tagged element, String[] tags) {
330 		if (element == null)
331 			return null;
332 		else {
333 			for (String tag : tags) {
334 				boolean invert = tag.startsWith("-");
335 				boolean containsTag = element.containsTag(invert ? tag.substring(1) : tag);
336 				if (invert ? containsTag : !containsTag) return false;
337 			}
338 			return true;
339 		}
340 	}
341 
342 	/**
343 	 * Extracts a name part of a hierarchic package, family or vendor name.
344 	 *
345 	 * @param name           the name to work on.
346 	 * @param delimiterCount the count of delimiters to include (e.g. 1 => vendorName, 2 => familyName, ...).
347 	 * @return A name with all trailing name parts stripped.
348 	 */
349 	public static String extractName(String name, int delimiterCount) {
350 		int endIdx = -1;
351 		for (int i = 0; i < delimiterCount; i++)
352 			endIdx = name.indexOf(':', endIdx + 1);
353 
354 		return endIdx == -1 ? name : name.substring(0, endIdx);
355 	}
356 
357 	/**
358 	 * Appends exactly one element to the given result list (using 'null' if the query produces no results).
359 	 *
360 	 * @param result the result list to add the element of the given query or 'null'
361 	 *               if the query produced no results.
362 	 * @param query  the query that fetches the element to add to the result.
363 	 * @throws NonUniqueResultException In case of the query produced more than one result.
364 	 */
365 	public static <T> void appendSingleElementToList(final Collection<T> result, final TypedQuery<? extends T> query) {
366 		final List<? extends T> ts = query.getResultList();
367 
368 		if (ts.isEmpty())
369 			result.add(null);
370 		else if (ts.size() > 1)
371 			throw new NonUniqueResultException("More than one result returned for query " + query);
372 		else
373 			result.add(ts.get(0));
374 	}
375 
376 	/**
377 	 * Applies page number and size to the query.
378 	 *
379 	 * @param query      the query to apply the limits on.
380 	 * @param pageNumber the number of the page to query.
381 	 * @param pageSize   the size of a single page.
382 	 * @return a positioned and limited query.
383 	 */
384 	public static <Q extends Query> Q applyPage(Q query, int pageNumber, int pageSize) {
385 		query.setFirstResult(pageNumber * pageSize);
386 		query.setMaxResults(pageSize + 1);
387 		return query;
388 	}
389 
390 	/**
391 	 * Converts the given query to a list page.
392 	 *
393 	 * @param query         the query to convert.
394 	 * @param emptyListPage the list page template instance to use.
395 	 * @return the given list page, after filling the query results.
396 	 */
397 	@SuppressWarnings("unchecked")
398 	public static <E, T extends AbstractListPage<E>> T toListPage(TypedQuery<E> query, T emptyListPage) {
399 		return toListPage(query, (Callback<E, E>) SINGLE_ELEMENT_CALLBACK, emptyListPage);
400 	}
401 
402 	/**
403 	 * Converts the given query to a list page using converterCallback to convert results.
404 	 *
405 	 * @param query             the query to convert.
406 	 * @param converterCallback a converter used to convert the query results to the type needed
407 	 *                          within the list page.
408 	 * @param emptyListPage     the list page template instance to use.
409 	 * @return the given list page, after filling the query results.
410 	 */
411 	public static <E, V, T extends AbstractListPage<E>> T toListPage(
412 			TypedQuery<V> query, Callback<V, E> converterCallback, T emptyListPage) {
413 		return toListPage((Query) query, converterCallback, emptyListPage);
414 	}
415 
416 	/**
417 	 * Converts the given query to a list page using converterCallback to convert results.
418 	 *
419 	 * @param query             the query to convert.
420 	 * @param converterCallback a converter used to convert the query results to the type needed
421 	 *                          within the list page.
422 	 * @param emptyListPage     the list page template instance to use.
423 	 * @return the given list page, after filling the query results.
424 	 */
425 	@SuppressWarnings("unchecked")
426 	public static <E, V, T extends AbstractListPage<E>> T toListPage(Query query, Callback<V, E> converterCallback, T emptyListPage) {
427 
428 		final int pageSize = query.getMaxResults() - 1, pageNumber = query.getFirstResult() / pageSize;
429 
430 		final List<V> results = (List<V>) query.getResultList();
431 		final List<E> pageElements = new ArrayList<E>(results.size());
432 
433 		int count = 0;
434 		for (V result : results) {
435 			if (++count > pageSize) break;
436 
437 			pageElements.add(converterCallback.call(result));
438 		}
439 
440 		if (pageElements.isEmpty()) return null;
441 
442 		emptyListPage.setPageNumber(pageNumber);
443 		emptyListPage.setLastPage(count <= pageSize);
444 		emptyListPage.setElements(pageElements);
445 		return emptyListPage;
446 	}
447 
448 	/**
449 	 * Converts the given query to a name list page.
450 	 *
451 	 * @param query      the query to convert.
452 	 * @param pageNumber the number of the page to extract.
453 	 * @param pageSize   the size of the page to return.
454 	 * @return the list page.
455 	 */
456 	@SuppressWarnings("unchecked")
457 	public static NameListPage toNameListPage(Query query, int pageNumber, int pageSize) {
458 		applyPage(query, pageNumber, pageSize);
459 		return toListPage(query, (Callback<String, String>) SINGLE_ELEMENT_CALLBACK, new NameListPage());
460 	}
461 
462 	/**
463 	 * Converts the given query to a name list page.
464 	 *
465 	 * @param query      the query to convert.
466 	 * @param pageNumber the number of the page to extract.
467 	 * @param pageSize   the size of the page to return.
468 	 * @return the list page.
469 	 */
470 	public static NameListPage toNameListPage(TypedQuery<String> query, int pageNumber, int pageSize) {
471 		return toNameListPage((Query) query, pageNumber, pageSize);
472 	}
473 
474 	private JpaUtils() {
475 	}
476 }