ref #7155, ref #7198, ref #7199 make Cdm.equals final
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / common / CdmBase.java
1 /**
2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.model.common;
10
11 import java.beans.PropertyChangeEvent;
12 import java.beans.PropertyChangeListener;
13 import java.beans.PropertyChangeSupport;
14 import java.io.Serializable;
15 import java.lang.reflect.Method;
16 import java.util.HashSet;
17 import java.util.List;
18 import java.util.Set;
19 import java.util.UUID;
20
21 import javax.persistence.Basic;
22 import javax.persistence.Column;
23 import javax.persistence.FetchType;
24 import javax.persistence.GeneratedValue;
25 import javax.persistence.Id;
26 import javax.persistence.ManyToOne;
27 import javax.persistence.MappedSuperclass;
28 import javax.persistence.Transient;
29 import javax.validation.constraints.Min;
30 import javax.validation.constraints.NotNull;
31 import javax.xml.bind.annotation.XmlAccessType;
32 import javax.xml.bind.annotation.XmlAccessorType;
33 import javax.xml.bind.annotation.XmlAttribute;
34 import javax.xml.bind.annotation.XmlElement;
35 import javax.xml.bind.annotation.XmlID;
36 import javax.xml.bind.annotation.XmlIDREF;
37 import javax.xml.bind.annotation.XmlSchemaType;
38 import javax.xml.bind.annotation.XmlTransient;
39 import javax.xml.bind.annotation.XmlType;
40 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
41
42 import org.apache.log4j.Logger;
43 import org.hibernate.annotations.NaturalId;
44 import org.hibernate.annotations.Type;
45 import org.hibernate.envers.Audited;
46 import org.hibernate.search.annotations.Analyze;
47 import org.hibernate.search.annotations.DocumentId;
48 import org.hibernate.search.annotations.Field;
49 import org.hibernate.search.annotations.FieldBridge;
50 import org.hibernate.search.annotations.Index;
51 import org.hibernate.search.annotations.Store;
52 import org.joda.time.DateTime;
53
54 import eu.etaxonomy.cdm.common.CdmUtils;
55 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
56 import eu.etaxonomy.cdm.hibernate.search.DateTimeBridge;
57 import eu.etaxonomy.cdm.hibernate.search.NotNullAwareIdBridge;
58 import eu.etaxonomy.cdm.hibernate.search.UuidBridge;
59 import eu.etaxonomy.cdm.jaxb.DateTimeAdapter;
60 import eu.etaxonomy.cdm.jaxb.UUIDAdapter;
61 import eu.etaxonomy.cdm.model.NewEntityListener;
62 import eu.etaxonomy.cdm.strategy.match.IMatchStrategy;
63 import eu.etaxonomy.cdm.strategy.match.IMatchable;
64 import eu.etaxonomy.cdm.strategy.match.Match;
65 import eu.etaxonomy.cdm.strategy.match.MatchMode;
66
67
68
69
70 /**
71 * The base class for all CDM domain classes implementing UUIDs and bean property change event firing.
72 * It provides a globally unique UUID and keeps track of creation date and person.
73 * The UUID is the same for different versions (see {@link VersionableEntity}) of a CDM object, so a locally unique id exists in addition
74 * that allows to safely access and store several objects (=version) with the same UUID.
75 *
76 * This class together with the {@link eu.etaxonomy.cdm.aspectj.PropertyChangeAspect}
77 * will fire bean change events to all registered listeners. Listener registration and event firing
78 * is done with the help of the {@link PropertyChangeSupport} class.
79 *
80 * @author m.doering
81 *
82 */
83 @XmlAccessorType(XmlAccessType.FIELD)
84 @XmlType(name = "CdmBase", propOrder = {
85 "created",
86 "createdBy"
87 })
88 @MappedSuperclass
89 public abstract class CdmBase implements Serializable, ICdmBase, ISelfDescriptive, Cloneable{
90 private static final long serialVersionUID = -3053225700018294809L;
91 @SuppressWarnings("unused")
92 private static final Logger logger = Logger.getLogger(CdmBase.class);
93
94 @Transient
95 @XmlTransient
96 private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
97
98 @Transient
99 @XmlTransient
100 private static NewEntityListener newEntityListener;
101
102 //@XmlAttribute(name = "id", required = true)
103 @XmlTransient
104 @Id
105 // @GeneratedValue(generator = "system-increment") //see also AuditEvent.revisionNumber
106 // @GeneratedValue(generator = "enhanced-table")
107 @GeneratedValue(generator = "custom-enhanced-table")
108 @DocumentId
109 @FieldBridge(impl=NotNullAwareIdBridge.class)
110 @Match(MatchMode.IGNORE)
111 @NotNull
112 @Min(0)
113 @Audited
114 private int id;
115
116 @XmlAttribute(required = true)
117 @XmlJavaTypeAdapter(UUIDAdapter.class)
118 @XmlID
119 @Type(type="uuidUserType")
120 @NaturalId // This has the effect of placing a "unique" constraint on the database column
121 @Column(length=36) //TODO needed? Type UUID will always assure that is exactly 36
122 @Match(MatchMode.IGNORE)
123 @NotNull
124 @Field(store = Store.YES, index = Index.YES, analyze = Analyze.NO)
125 @FieldBridge(impl = UuidBridge.class)
126 @Audited
127 protected UUID uuid;
128
129 @XmlElement (name = "Created", type= String.class)
130 @XmlJavaTypeAdapter(DateTimeAdapter.class)
131 @Type(type="dateTimeUserType")
132 @Basic(fetch = FetchType.LAZY)
133 @Match(MatchMode.IGNORE)
134 @Field(analyze = Analyze.NO)
135 @FieldBridge(impl = DateTimeBridge.class)
136 @Audited
137 private DateTime created;
138
139 @XmlElement (name = "CreatedBy")
140 @XmlIDREF
141 @XmlSchemaType(name = "IDREF")
142 @ManyToOne(fetch=FetchType.LAZY)
143 @Match(MatchMode.IGNORE)
144 @Audited
145 private User createdBy;
146
147 /**
148 * Class constructor assigning a unique UUID and creation date.
149 * UUID can be changed later via setUuid method.
150 */
151 public CdmBase() {
152 this.uuid = UUID.randomUUID();
153 this.created = new DateTime().withMillisOfSecond(0);
154 }
155
156 public static void setNewEntityListener(NewEntityListener nel) {
157 newEntityListener = nel;
158 }
159
160 public static void fireOnCreateEvent(CdmBase cdmBase) {
161 if(newEntityListener != null) {
162 newEntityListener.onCreate(cdmBase);
163 }
164 }
165
166 /**
167 * see {@link PropertyChangeSupport#addPropertyChangeListener(PropertyChangeListener)}
168 * @param listener
169 */
170 public void addPropertyChangeListener(PropertyChangeListener listener) {
171 propertyChangeSupport.addPropertyChangeListener(listener);
172 }
173
174 /**
175 * see {@link PropertyChangeSupport#addPropertyChangeListener(String, PropertyChangeListener)}
176 */
177 public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
178 propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
179 }
180
181 /**
182 * see {@link PropertyChangeSupport#addPropertyChangeListener(PropertyChangeListener)}
183 */
184 public void removePropertyChangeListener(PropertyChangeListener listener) {
185 propertyChangeSupport.removePropertyChangeListener(listener);
186 }
187
188 /**
189 * @see PropertyChangeSupport#addPropertyChangeListener(String, PropertyChangeListener)
190 */
191 public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
192 propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
193 }
194
195 public boolean hasListeners(String propertyName) {
196 return propertyChangeSupport.hasListeners(propertyName);
197 }
198
199 public void firePropertyChange(String property, String oldval, String newval) {
200 propertyChangeSupport.firePropertyChange(property, oldval, newval);
201 }
202 public void firePropertyChange(String property, int oldval, int newval) {
203 propertyChangeSupport.firePropertyChange(property, oldval, newval);
204 }
205 public void firePropertyChange(String property, float oldval, float newval) {
206 propertyChangeSupport.firePropertyChange(property, oldval, newval);
207 }
208 public void firePropertyChange(String property, boolean oldval, boolean newval) {
209 propertyChangeSupport.firePropertyChange(property, oldval, newval);
210 }
211 public void firePropertyChange(String property, Object oldval, Object newval) {
212 propertyChangeSupport.firePropertyChange(property, oldval, newval);
213 }
214 public void firePropertyChange(PropertyChangeEvent evt) {
215 propertyChangeSupport.firePropertyChange(evt);
216 }
217
218 /**
219 * This method was initially added to {@link CdmBase} to fix #5161.
220 * It can be overridden by subclasses such as {@link IdentifiableEntity}
221 * to explicitly initialize listeners. This is needed e.g. after de-serialization
222 * as listeners are not serialized due to the @Transient annotation.
223 * However, it can be generally used for other use-cases as well
224 */
225 public void initListener() {}
226
227 /**
228 * Adds an item to a set of <code>this</code> object and fires the according
229 * {@link PropertyChangeEvent}. Workaround as long as add and remove is not yet
230 * implemented in aspectJ.
231 * @param set the set the new item is added to
232 * @param newItem the new item to be added to the set
233 * @param propertyName the name of the set as property in <code>this</code> object
234 */
235 protected <T extends CdmBase> void addToSetWithChangeEvent(Set<T> set, T newItem, String propertyName ){
236 Set<T> oldValue = new HashSet<>(set);
237 set.add(newItem);
238 firePropertyChange(new PropertyChangeEvent(this, propertyName, oldValue, set));
239 }
240
241 /**
242 * Removes an item from a set of <code>this</code> object and fires the according
243 * {@link PropertyChangeEvent}. Workaround as long as add and remove is not yet
244 * implemented in aspectJ.
245 * @param set the set the item is to be removed from
246 * @param itemToRemove the item to be removed from the set
247 * @param propertyName the name of the set as property in <code>this</code> object
248 */
249 protected <T extends CdmBase> void removeFromSetWithChangeEvent(Set<T> set, T itemToRemove, String propertyName ){
250 Set<T> oldValue = new HashSet<T>(set);
251 set.remove(itemToRemove);
252 firePropertyChange(new PropertyChangeEvent(this, propertyName, oldValue, set));
253 }
254
255 @Override
256 public UUID getUuid() {
257 return uuid;
258 }
259 @Override
260 public void setUuid(UUID uuid) {
261 this.uuid = uuid;
262 }
263
264 @Override
265 public int getId() {
266 return this.id;
267 }
268 @Override
269 public void setId(int id) { //see #265 (private ?)
270 this.id = id;
271 }
272
273 @Override
274 public DateTime getCreated() {
275 return created;
276 }
277 @Override
278 public void setCreated(DateTime created) {
279 if (created != null){
280 created = created.withMillisOfSecond(0);
281 //created.set(Calendar.MILLISECOND, 0); //old, can be deleted
282 }
283 this.created = created;
284 }
285
286
287 @Override
288 public User getCreatedBy() {
289 return this.createdBy;
290 }
291 @Override
292 public void setCreatedBy(User createdBy) {
293 this.createdBy = createdBy;
294 }
295
296 // ************************** Hibernate proxies *******************/
297
298 /**
299 * If entity is a HibernateProxy it returns the initialized object.
300 * Otherwise entity itself is returned.
301 * @param entity
302 * @return
303 * @throws ClassCastException
304 */
305 public static <T> T deproxy(T entity) {
306 return HibernateProxyHelper.deproxy(entity);
307 }
308
309 /**
310 * These methods are present due to HHH-1517 - that in a one-to-many
311 * relationship with a superclass at the "one" end, the proxy created
312 * by hibernate is the superclass, and not the subclass, resulting in
313 * a classcastexception when you try to cast it.
314 *
315 * Hopefully this will be resolved through improvements with the creation of
316 * proxy objects by hibernate and the following methods will become redundant,
317 * but for the time being . . .
318 * @param <T>
319 * @param object
320 * @param clazz
321 * @return
322 * @throws ClassCastException
323 */
324 //non-static does not work because javassist already unwrapps the proxy before calling the method
325 public static <T extends CdmBase> T deproxy(Object object, Class<T> clazz) throws ClassCastException {
326 return HibernateProxyHelper.deproxy(object, clazz);
327 }
328
329 @Override
330 public boolean isInstanceOf(Class<? extends CdmBase> clazz) throws ClassCastException {
331 return HibernateProxyHelper.isInstanceOf(this, clazz);
332 }
333
334 // ************* Object overrides *************************/
335
336 /**
337 * Is <code>true</code> if UUID and created timestamp (is this really needed/make sense?)
338 * is the same for the passed Object and this one.
339 * This method is final as subclasses should not override it.
340 * The contract should be the same for all persistable entities.
341 * 2 instances are equal if they represent the same entity in a given
342 * database.
343 * <BR><BR>
344 *
345 * If one wants to compare 2 CdmBase entities content wise you may use e.g. a
346 * {@link IMatchStrategy match strategy} and make sure
347 * {@link IMatchable matching} is implemented for the respective CdmBase subclass.
348 * You may adapt your match strategy to your own needs.
349 *
350 * @see java.lang.Object#equals(java.lang.Object)
351 * See {@link http://www.hibernate.org/109.html hibernate109}, {@link http://www.geocities.com/technofundo/tech/java/equalhash.html geocities}
352 * or {@link http://www.ibm.com/developerworks/java/library/j-jtp05273.html ibm}
353 * for more information about equals and hashcode.
354 */
355 @Override
356 public final boolean equals(Object obj) {
357 if (obj == this){
358 return true;
359 }
360 if (obj == null){
361 return false;
362 }
363 if (!CdmBase.class.isAssignableFrom(obj.getClass())){
364 return false;
365 }
366 ICdmBase cdmObj = (ICdmBase)obj;
367 UUID objUuid = cdmObj.getUuid();
368 if (objUuid == null){
369 throw new NullPointerException("CdmBase is missing UUID");
370 }
371 boolean uuidEqual = objUuid.equals(this.getUuid());
372 //TODO is this still needed?
373 boolean createdEqual = CdmUtils.nullSafeEqual(cdmObj.getCreated(), this.getCreated());
374 if (! uuidEqual || !createdEqual){
375 return false;
376 }
377 return true;
378 }
379
380
381
382 /** Overrides {@link java.lang.Object#hashCode()}
383 * See {@link http://www.hibernate.org/109.html hibernate109}, {@link http://www.geocities.com/technofundo/tech/java/equalhash.html geocities}
384 * or {@link http://www.ibm.com/developerworks/java/library/j-jtp05273.html ibm}
385 * for more information about equals and hashcode.
386 */
387 @Override
388 public int hashCode() {
389 int hashCode = 7;
390 if(this.getUuid() != null) {
391 //this unfortunately leads to errors when loading maps via hibernate
392 //as hibernate computes hash values for CdmBase objects used as key at
393 // a time when the uuid is not yet loaded from the database. Therefore
394 //the hash values later change and give wrong results when retrieving
395 //data from the map (map.get(key) returns null, though there is an entry
396 //for key in the map.
397 //see further comments in #2114
398 int result = 29 * hashCode + this.getUuid().hashCode();
399 // int shresult = 29 * hashCode + Integer.valueOf(this.getId()).hashCode();
400 return result;
401 } else {
402 return 29 * hashCode;
403 }
404 }
405
406 /**
407 * Overrides {@link java.lang.Object#toString()}.
408 * This returns an String that identifies the object well without being necessarily unique. Internally the method is delegating the
409 * call to {link {@link #instanceToString()}.<br>
410 * <b>Specification:</b> This method should never call other object' methods so it can be well used for debugging
411 * without problems like lazy loading, unreal states etc.
412 * <p>
413 * <b>Note</b>: If overriding this method's javadoc always copy or link the above requirement.
414 * If not overwritten by a subclass method returns the class, id and uuid as a string for any CDM object.
415 * <p>
416 * <b>For example</b>: Taxon#13&lt;b5938a98-c1de-4dda-b040-d5cc5bfb3bc0&gt;
417 * @see java.lang.Object#toString()
418 */
419 @Override
420 public String toString() {
421 return instanceToString();
422 }
423
424 /**
425 * This returns an String that identifies the cdm instance well without being necessarily unique.
426 * The string representation combines the class name the {@link #id} and {@link #uuid}.
427 * <p>
428 * <b>For example</b>: Taxon#13&lt;b5938a98-c1de-4dda-b040-d5cc5bfb3bc0&gt;
429 * @return
430 */
431 public String instanceToString() {
432 return this.getClass().getSimpleName()+"#"+this.getId()+"<"+this.getUuid()+">";
433 }
434
435 // **************** invoke methods **************************/
436
437 protected void invokeSetMethod(Method method, Object object){
438 try {
439 method.invoke(object, this);
440 } catch (Exception e) {
441 e.printStackTrace();
442 //TODO handle exceptioin;
443 }
444 }
445
446 protected void invokeSetMethodWithNull(Method method, Object object){
447 try {
448 Object[] nul = new Object[]{null};
449 method.invoke(object, nul);
450 } catch (Exception e) {
451 e.printStackTrace();
452 //TODO handle exceptioin;
453 }
454 }
455
456 @Transient
457 @Override
458 public String getUserFriendlyTypeName(){
459 return getClass().getSimpleName();
460 }
461
462 @Transient
463 @Override
464 public String getUserFriendlyDescription(){
465 return toString();
466 }
467
468 @Override
469 public String getUserFriendlyFieldName(String field){
470 return field;
471 }
472
473 // ********************* HELPER ****************************************/
474
475 protected <T extends CdmBase> boolean replaceInList(List<T> list,
476 T newObject, T oldObject){
477 boolean result = false;
478 for (int i = 0; i < list.size(); i++){
479 if (list.get(i).equals(oldObject)){
480 list.set(i, newObject);
481 result = true;
482 }
483 }
484 return result;
485 }
486
487
488 /**
489 * Returns true if the given String is blank.
490 * @param str the String to check
491 * @see CdmUtils#isBlank(String)
492 * @return true if str is blank
493 */
494 protected boolean isBlank(String str) {
495 return CdmUtils.isBlank(str);
496 }
497
498
499
500 //********************** CLONE *****************************************/
501
502 // protected void clone(CdmBase clone){
503 // clone.setCreatedBy(createdBy);
504 // clone.setId(id);
505 // clone.propertyChangeSupport=new PropertyChangeSupport(clone);
506 // //Constructor Attributes
507 // //clone.setCreated(created);
508 // //clone.setUuid(getUuid());
509 //
510 // }
511
512 @Override
513 public Object clone() throws CloneNotSupportedException{
514 CdmBase result = (CdmBase)super.clone();
515 result.propertyChangeSupport=new PropertyChangeSupport(result);
516
517 //TODO ?
518 result.setId(0);
519 result.setUuid(UUID.randomUUID());
520 result.setCreated(new DateTime());
521 result.setCreatedBy(null);
522
523 //no changes to: -
524 return result;
525 }
526
527 }