View Javadoc

1   /*
2    * EL4J, the Extension Library for the J2EE, adds incremental enhancements to
3    * the spring framework, http://el4j.sf.net
4    * Copyright (C) 2008 by ELCA Informatique SA, Av. de la Harpe 22-24,
5    * 1000 Lausanne, Switzerland, http://www.elca.ch
6    *
7    * EL4J is published under the GNU Lesser General Public License (LGPL)
8    * Version 2.1. See http://www.gnu.org/licenses/
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13   * GNU Lesser General Public License for more details.
14   *
15   * For alternative licensing, please contact info@elca.ch
16   */
17  package ch.elca.el4j.services.persistence.hibernate.dao.extent;
18  
19  
20  import java.lang.reflect.Method;
21  import java.lang.reflect.ParameterizedType;
22  import java.lang.reflect.Type;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Iterator;
26  import java.util.LinkedList;
27  import java.util.List;
28  
29  import org.springframework.util.Assert;
30  
31  import ch.elca.el4j.core.metadata.ContainedClass;
32  import ch.elca.el4j.util.codingsupport.BeanPropertyUtils;
33  
34  
35  /**
36   * A ExtentEntity represents a complex Data Type in an Extent.
37   * <br>
38   * Features: <br>
39   *  <ul>
40   *   <li> static method entity: create a new entity 
41   *   <li> with/without: add/remove fields, sub-entities and collections to/from the extent
42   *   <li> withSubentities: add sub-entities to the extent, convenient for adding entities you want
43   *   		to define in detail.
44   *   <li> all: the whole entity with all fields, entities and collections.
45   *  </ul>
46   *
47   *
48   * @svnLink $Revision: 3875 $;$Date: 2009-08-04 14:35:53 +0200 (Di, 04. Aug 2009) $;$Author: swismer $;$URL: https://el4j.svn.sourceforge.net/svnroot/el4j/branches/el4j_3_1/el4j/framework/modules/hibernate/src/main/java/ch/elca/el4j/services/persistence/hibernate/dao/extent/ExtentEntity.java $
49   *
50   * @author Andreas Rueedlinger (ARR)
51   */
52  public class ExtentEntity extends AbstractExtentPart {
53  	
54  	/** The class of the entity. */
55  	private Class<?> m_entityClass;
56  	
57  	/** The field-methods of the entity, always stays sorted. */
58  	private List<String> m_fields;
59  	
60  	/** The child-entities of the entity, always stays sorted by name. */
61  	private List<ExtentEntity> m_childEntities;
62  	
63  	/** The collections of the entity, always stays sorted by name. */
64  	private List<ExtentCollection> m_collections;
65  	
66  	/** The id of the entity. */
67  	private String m_entityId;
68  	
69  	/** Is the entity a root entity. */
70  	private boolean m_root = false;
71  	
72  	/** Is the ExtentEntity frozen, eg. must not be changed anymore. */
73  	private boolean m_frozen;
74  	
75  	/**
76  	 * Default Creator, hidden.
77  	 * @param c		the class of the entity
78  	 */
79  	public ExtentEntity(Class<?> c) {
80  		m_name = firstCharLower(c.getSimpleName());
81  		m_entityClass = c;
82  		m_fields = new LinkedList<String>();
83  		m_childEntities = new LinkedList<ExtentEntity>();
84  		m_collections = new LinkedList<ExtentCollection>();
85  		m_entityId = String.format("|%s[][][]|", m_entityClass.getName());
86  	}
87  	
88  	/**
89  	 * Default Creator, hidden.
90  	 * @param name	the name of the entity
91  	 * @param c		the class of the entity
92  	 */
93  	public ExtentEntity(String name, Class<?> c) {
94  		m_name = name;
95  		m_entityClass = c;
96  		m_fields = new LinkedList<String>();
97  		m_childEntities = new LinkedList<ExtentEntity>();
98  		m_collections = new LinkedList<ExtentCollection>();
99  		m_entityId = String.format("|%s[][][]|", m_name);
100 	}
101 	
102 	/**
103 	 * Default Creator, hidden.
104 	 * @param c 		the class of the entity
105 	 * @param method	the method to get the entity
106 	 */
107 	public ExtentEntity(Class<?> c, Method method) {
108 		m_name = toFieldName(method);
109 		m_entityClass = c;
110 		//m_method = method;
111 		m_fields = new LinkedList<String>();
112 		m_childEntities = new LinkedList<ExtentEntity>();
113 		m_collections = new LinkedList<ExtentCollection>();
114 		m_entityId = String.format("|%s[][][]|", m_name);
115 	}
116 	
117 	/** {@inheritDoc} */
118 	public String getId() {
119 		return m_entityId;
120 	}
121 	
122 	/** {@inheritDoc} */
123 	protected void updateId() {
124 		rebuildId();
125 	}
126 	
127 	/**
128 	 * @return if the entity is a root entity.
129 	 */
130 	public boolean isRoot() {
131 		return m_root;
132 	}
133 	
134 	/**
135 	 * Class of the entity.
136 	 * @return the class of the entity.
137 	 */
138 	public Class<?> getEntityClass() {
139 		return m_entityClass;
140 	}
141 	
142 	/**
143 	 * Field-methods of the entity.
144 	 * @return the field-methods of the entity.
145 	 */
146 	public List<String> getFields() {
147 		return new LinkedList<String>(m_fields);
148 	}
149 	
150 	/**
151 	 * Child entities.
152 	 * @return the child entities of the entity.
153 	 */
154 	public List<ExtentEntity> getChildEntities() {
155 		return new LinkedList<ExtentEntity>(m_childEntities);
156 	}
157 	
158 	/**
159 	 * Collections.
160 	 * @return the collections of the entity.
161 	 */
162 	public List<ExtentCollection> getCollections() {
163 		return new LinkedList<ExtentCollection>(m_collections);
164 	}
165 	
166 	/**
167 	 * Rebuild the id string.
168 	 * Go recursive through all children and get their id.
169 	 * If entity is root and has a parent, infinite loops are prevented
170 	 * by not updating the parent and outputting the hashCode when to toString is called.
171 	 */
172 	private void rebuildId() {
173 		// Rebuild the id string
174 		String id = "|";
175 		
176 		if (isRoot()) {
177 			id += m_entityClass.getName();
178 		} else {
179 			id += m_name;
180 		}
181 		id += m_fields.toString();
182 		id += m_childEntities.toString();
183 		id += m_collections.toString();
184 		id += "|";
185 		
186 		// Inform the parent if id changed
187 		if (!m_entityId.equals(id)) {
188 			m_entityId = id;
189 			if (m_parent != null && !isRoot()) {
190 				m_parent.updateId();
191 			}
192 		}
193 	}
194 	/**
195 	 * Add a field-method to the fields of the entity.
196 	 * @param field	 the field-method to add.
197 	 * @throws NoSuchMethodException 
198 	 * @throws SecurityException 
199 	 */
200 	private void addField(String field) {
201 		if (!m_fields.contains(field)) {
202 			m_fields.add(field);
203 			Collections.sort(m_fields);
204 			rebuildId();
205 		}
206 	}
207 	
208 	/**
209 	 * Remove a field from the fields of the entity as name.
210 	 * @param name	 the field as name to remove.
211 	 * @return returns the success of the operation
212 	 */
213 	private boolean removeField(String name) {
214 		if (m_fields.remove(name)) {
215 			rebuildId();
216 			return true;
217 		} else {
218 			return false;
219 		}
220 	}
221 	
222 	/**
223 	 * Add a child-entity to the entity. Parent of child entity is set and consistency
224 	 * to parent class is checked.
225 	 * @param child	 the child to add.
226 	 * @throws NoSuchMethodException 
227 	 */
228 	private void addChildEntity(ExtentEntity child) throws NoSuchMethodException {
229 		if (!m_childEntities.contains(child)) {
230 			// Merge entity with already existing one when same name
231 			for (ExtentEntity ent : m_childEntities) {
232 				if (ent.m_name.equals(child.m_name)) {
233 					try {
234 						BeanPropertyUtils.getReadMethod(m_entityClass, child.getName());
235 						ent.merge(child);
236 						return;
237 					} catch (IllegalArgumentException e) {
238 						throw new NoSuchMethodException(e.getMessage());
239 					}
240 				}
241 			}
242 			try {
243 				Method m  = BeanPropertyUtils.getReadMethod(m_entityClass, child.getName());
244 				if (m != null) {
245 					child.setParent(this);
246 					m_childEntities.add(child);
247 					Collections.sort(m_childEntities);
248 					rebuildId();
249 				}
250 			} catch (IllegalArgumentException e) {
251 				throw new NoSuchMethodException(e.getMessage());
252 			}
253 		}
254 	}
255 	
256 	/**
257 	 * Remove an entity from the children of the entity.
258 	 * @param name	 name of the entity to remove.
259 	 * @return returns the success of the operation
260 	 */
261 	private boolean removeEntity(String name) {
262 		for (ExtentEntity e : m_childEntities) {
263 			if (e.getName().equals(name)) {
264 				m_childEntities.remove(e);
265 				rebuildId();
266 				return true;
267 			}
268 		}
269 		return false;
270 	}
271 	
272 	/**
273 	 * Add a collection to the entity.
274 	 * @param collection	 the collection to add.
275 	 * @throws NoSuchMethodException 
276 	 */
277 	private void addCollection(ExtentCollection collection) throws NoSuchMethodException {
278 		if (!m_collections.contains(collection)) {
279 			// Merge collection with already existing one when same name
280 			for (ExtentCollection c : m_collections) {
281 				if (c.m_name.equals(collection.m_name)) {
282 					try {
283 						BeanPropertyUtils.getReadMethod(m_entityClass, collection.getName());
284 						c.merge(collection);
285 						return;
286 					} catch (IllegalArgumentException e) {
287 						throw new NoSuchMethodException(e.getMessage());
288 					}
289 				}
290 			}
291 			try {
292 				Method m = BeanPropertyUtils.getReadMethod(m_entityClass, collection.getName());
293 				collection.setParent(this);
294 				boolean consistent = false;
295 				// Check if the class of the contained entity is consistent
296 				Type rawType = m.getGenericReturnType();
297 				if (rawType instanceof ParameterizedType) {
298 					Type[] pt = ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments();
299 					if (pt.length > 0 && pt[0] instanceof Class<?>) {
300 						if (((Class<?>) pt[0]).isAssignableFrom(collection.getContainedEntity().getEntityClass())) {
301 
302 							consistent = true;
303 						}
304 					}
305 				}
306 				if (consistent) {
307 					m_collections.add(collection);
308 					Collections.sort(m_collections);
309 					rebuildId();
310 				} else {
311 					throw new NoSuchMethodException("Collection type ["
312 						+ collection.getContainedEntity().getEntityClass().getSimpleName()
313 						+ "] doesnt conform with class definition.");
314 				}
315 			} catch (IllegalArgumentException e) {
316 				throw new NoSuchMethodException(e.getMessage());
317 			}
318 		}
319 	}
320 	
321 	/**
322 	 * Remove a collection from the collections of the entity.
323 	 * @param name	 name of the collection to remove.
324 	 * @return returns the success of the operation
325 	 */
326 	private boolean removeCollection(String name) {
327 		for (ExtentCollection e : m_collections) {
328 			if (e.getName().equals(name)) {
329 				m_collections.remove(e);
330 				rebuildId();
331 				return true;
332 			}
333 		}
334 		return false;
335 	}
336 	
337 	/**
338 	 * Add a method to the extent (given as name).
339 	 * Automatically checks if a property, entity or a collection.
340 	 * @param name	 the field to add.
341 	 * @throws NoSuchMethodException 
342 	 */
343 	private void addMethodAsName(String name) throws NoSuchMethodException {
344 		try {
345 			Method m = BeanPropertyUtils.getReadMethod(m_entityClass, name);
346 			if (m != null) {
347 				fetchMethod(m, DataExtent.DEFAULT_LOADING_DEPTH);
348 			} else {
349 				throw new NoSuchMethodException("Method doesn't exist.");
350 			}
351 		} catch (IllegalArgumentException e) {
352 			throw new NoSuchMethodException(e.getMessage());
353 		}
354 	}
355 	
356 	
357 	//*************** Fluent API ******************//
358 	
359 	
360 	/**
361 	 * Returns a new Entity object, based on the given class.
362 	 * @param c		the class of the entity.
363 	 * @return	the Entity object.
364 	 */
365 	public static ExtentEntity rootEntity(Class<?> c) {
366 		ExtentEntity tmp = new ExtentEntity(c);
367 		tmp.m_root = true;
368 		return tmp;
369 		
370 	}
371 	
372 	/**
373 	 * Returns a new Entity object, based on the given class.
374 	 * @param c		the class of the entity.
375 	 * @return	the Entity object.
376 	 */
377 	public static ExtentEntity entity(Class<?> c) {
378 		return new ExtentEntity(c);
379 	}
380 	
381 	/**
382 	 * Returns a new Entity object, based on the given name and class.
383 	 * @param name	the name of the entity.
384 	 * @param c		the class of the entity.
385 	 * @return	the Entity object.
386 	 */
387 	public static ExtentEntity entity(String name, Class<?> c) {
388 		return new ExtentEntity(name, c);
389 	}
390 	
391 	/**
392 	 * Returns a new Entity object, based on the given class and method.
393 	 * @param c		the class of the entity.
394 	 * @param m		the method to get the entity.
395 	 * @return	the Entity object.
396 	 */
397 	public static ExtentEntity entity(Class<?> c, Method m) {
398 		return new ExtentEntity(c, m);
399 	}
400 	
401 	/**
402 	 * Extend the entity by the given fields.
403 	 * Fields are either simple properties, sub-entities or collections.
404 	 * @param fields	fields to be added.
405 	 * 
406 	 * @return the new ExtentEntity Object.
407 	 * @throws NoSuchMethodException 
408 	 */
409 	public ExtentEntity with(String... fields) throws NoSuchMethodException {
410 		Assert.state(!m_frozen, "DataExtent is frozen and cannot be changed anymore");
411 		for (String s : fields) {
412 			addMethodAsName(s);
413 		}
414 		return this;
415 	}
416 	
417 	/**
418 	 * Extend the entity by the given sub-entities.
419 	 * @param entities	entities to be added.
420 	 * 
421 	 * @return the new ExtentEntity Object.
422 	 * @throws NoSuchMethodException 
423 	 */
424 	public ExtentEntity withSubentities(AbstractExtentPart... entities) throws NoSuchMethodException {
425 		Assert.state(!m_frozen, "DataExtent is frozen and cannot be changed anymore");
426 		for (AbstractExtentPart entity : entities) {
427 			if (entity instanceof ExtentEntity) {
428 				addChildEntity((ExtentEntity) entity);
429 			} else {
430 				addCollection((ExtentCollection) entity);
431 			}
432 		}
433 		return this;
434 	}
435 	
436 	/**
437 	 * Exclude fields from the entity.
438 	 * Fields are either simple properties, sub-entities or collections.
439 	 * @param fields	fields to be excluded.
440 	 * 
441 	 * @return the new ExtentEntity Object.
442 	 */
443 	public ExtentEntity without(String...fields) {
444 		Assert.state(!m_frozen, "DataExtent is frozen and cannot be changed anymore");
445 		for (String s : fields) {
446 			if (!removeField(s)) {
447 				if (!removeEntity(s)) {
448 					removeCollection(s);
449 				}
450 			}
451 		}
452 		return this;
453 	}
454 	
455 	/**
456 	 * Include all fields, entities and collections of the class-entity.
457 	 * @param depth		Exploration depth.
458 	 * @return the new ExtentEntity Object.
459 	 */
460 	public ExtentEntity all(int depth) {
461 		Assert.state(!m_frozen, "DataExtent is frozen and cannot be changed anymore");
462 		if (depth > 0) {
463 			for (Method m : m_entityClass.getMethods()) {
464 				try {
465 					fetchMethod(m, depth);
466 				} catch (NoSuchMethodException e) {
467 					// not possible since we found the method!
468 				}
469 			}
470 		}
471 		return this;
472 	}
473 	
474 	/**
475 	 * Merge two ExtentEntities. Returns the union of the entities.
476 	 * The class of the two entities should be the same, the name and the parent is taken from 
477 	 * this object.
478 	 * @param other	the extent to be merged with.
479 	 * @return	the merged entity.
480 	 */
481 	public ExtentEntity merge(ExtentEntity other) {
482 		Assert.state(!m_frozen, "DataExtent is frozen and cannot be changed anymore");
483 		if (m_entityClass.equals(other.m_entityClass) && !this.equals(other)) {
484 			mergeFields(other.m_fields);
485 			mergeEntities(other.m_childEntities);
486 			mergeCollections(other.m_collections);
487 			rebuildId();
488 		}
489 		return this;
490 	}
491 	
492 	/**
493 	 * Freeze the ExtentEntity, meaning that no further changes to it are possible.
494 	 * @return the frozen ExtentEntity.
495 	 */
496 	public ExtentEntity freeze() {
497 		if (!m_frozen) {
498 			m_frozen = true;
499 			for (ExtentEntity ent : m_childEntities) {
500 				ent.freeze();
501 			}
502 			for (ExtentCollection c : m_collections) {
503 				c.freeze();
504 			}
505 		}
506 		return this;
507 	}
508 	
509 	/**
510 	 * Merge the List of fields into the current  field list.
511 	 * @param otherFields the fields to merge with.
512 	 */
513 	private void mergeFields(List<String> otherFields) {
514 		// Merge fields
515 		if (m_fields.isEmpty()) {
516 			m_fields.addAll(otherFields);
517 		} else {
518 			Iterator<String> i = otherFields.iterator();
519 			String f = null;
520 			if (i.hasNext()) {
521 				f = i.next();
522 			}
523 			for (int k = 0; k < m_fields.size() && f != null; k++) {
524 				int result = m_fields.get(k).compareTo(f);
525 				if (result > 0) {
526 					// add element if it is before the current element
527 					m_fields.add(k, f);
528 				}
529 				if (result >= 0) {
530 					// Iterate to next element
531 					if (i.hasNext()) {
532 						f = i.next();
533 					} else {
534 						f = null;
535 					}
536 				}
537 			}
538 		}
539 	}
540 	
541 	/**
542 	 * Merge the List of entities into the current entity list.
543 	 * @param otherEntities the entities to merge with.
544 	 */
545 	private void mergeEntities(List<ExtentEntity> otherEntities) {
546 		// Merge entities
547 		if (m_childEntities.isEmpty()) {
548 			m_childEntities.addAll(otherEntities);
549 		} else {
550 			Iterator<ExtentEntity> i = otherEntities.iterator();
551 			ExtentEntity e = null;
552 			if (i.hasNext()) {
553 				e = i.next();
554 			}
555 			for (int k = 0; k < m_childEntities.size() && e != null; k++) {
556 				int result = m_childEntities.get(k).compareTo(e);
557 				if (result > 0) {
558 					// add element if it is before the current element
559 					m_childEntities.add(k, e);
560 				} else if (result == 0 && !e.equals(m_childEntities.get(k))) {
561 					// Merge the two child entities
562 					// TODO infinite loops??
563 					m_childEntities.get(k).merge(e);
564 				}
565 				if (result >= 0) {
566 					// Iterate to next element
567 					if (i.hasNext()) {
568 						e = i.next();
569 					} else {
570 						e = null;
571 					}
572 				}
573 			}
574 		}
575 	}
576 	
577 	/**
578 	 * Merge the List of entities into the current entity list.
579 	 * @param otherCollections the collections to merge with.
580 	 */
581 	private void mergeCollections(List<ExtentCollection> otherCollections) {
582 		// Merge collections
583 		if (m_collections.isEmpty()) {
584 			m_collections.addAll(otherCollections);
585 		} else {
586 			Iterator<ExtentCollection> i = otherCollections.iterator();
587 			ExtentCollection c = null;
588 			if (i.hasNext()) {
589 				c = i.next();
590 			}
591 			for (int k = 0; k < m_collections.size() && c != null; k++) {
592 				int result = m_collections.get(k).compareTo(c);
593 				if (result > 0) {
594 					// add element if it is before the current element
595 					m_collections.add(k, c);
596 				} else if (result == 0 
597 					&& !c.getContainedEntity().equals(m_collections.get(k).getContainedEntity())) {
598 					// Merge the two contained entities
599 					// TODO infinite loops??
600 					m_collections.get(k).merge(c);
601 				}
602 				if (result >= 0) {
603 					// Iterate to next element
604 					if (i.hasNext()) {
605 						c = i.next();
606 					} else {
607 						c = null;
608 					}
609 				}
610 			}
611 		}
612 	}
613 	
614 	/**
615 	 * Add the method to the entity in the appropriate manner,
616 	 * as field, entity or collection.
617 	 * Ensure when calling the function that method exists in entity class.
618 	 * @param m			the method to be added.
619 	 * @param depth		Exploration depth.
620 	 * @throws NoSuchMethodException 
621 	 */
622 	private void fetchMethod(Method m, int depth) throws NoSuchMethodException {
623 		// Fetch only the methods with getter and no arguments
624 		// Exclude getClass()
625 		boolean isGetter = m.getName().startsWith("get") && m.getParameterTypes().length == 0;
626 		if (isGetter && !m.getName().equals("getClass") && !m.getName().equals("get")) {
627 
628 			if (m.getReturnType().isPrimitive() || m.getReturnType().isEnum()
629 				/*|| m.getReturnType().equals(String.class)*/) {
630 				
631 				addField(toFieldName(m));
632 			} else if (Collection.class.isAssignableFrom(m.getReturnType())) {
633 				fetchCollection(m, depth);
634 			} else {
635 				ExtentEntity tmp = entity(m.getReturnType(), m);
636 				if (depth > 1) {
637 					tmp.all(depth - 1);
638 				}
639 				addChildEntity(tmp);
640 			}
641 		}
642 	}
643 	
644 	/**
645 	 * Add the collection method to the entity,
646 	 * with all its special treatment.
647 	 * @param m			the method to be added.
648 	 * @param depth		Exploration depth.
649 	 * @throws NoSuchMethodException 
650 	 */
651 	private void fetchCollection(Method m, int depth) throws NoSuchMethodException {
652 		// Special treatment if collection type
653 		Class<?> t = null;
654 		// First try to read out the contained type of the annotation
655 		ContainedClass containedAnnotation = m.getAnnotation(ContainedClass.class);
656 		if (containedAnnotation == null) {
657 			Type rawType = m.getGenericReturnType();
658 			if (rawType instanceof ParameterizedType) {
659 				Type[] pt = ((ParameterizedType) m.getGenericReturnType())
660 				.getActualTypeArguments();
661 				if (pt.length > 0 && pt[0] instanceof Class<?>) {
662 					t = (Class<?>) ((ParameterizedType) m.getGenericReturnType())
663 						.getActualTypeArguments()[0];
664 				}
665 			}
666 		} else {
667 			t = containedAnnotation.value();
668 		}
669 		if (t != null) {
670 			ExtentCollection tmp = new ExtentCollection(t, m);
671 			if (depth > 1) {
672 				tmp.getContainedEntity().all(depth - 1);
673 			}
674 			addCollection(tmp);
675 		}
676 		
677 	}
678 	
679 
680 	/** {@inheritDoc} */
681 	@Override
682 	public String toString() {
683 		if (m_parent != null && isRoot()) {
684 			return super.nativeToString();
685 		} else {
686 			return m_entityId;
687 		}
688 	}
689 	
690 }