View Javadoc

1   package org.starobjects.jpa.runtime.persistence.oid;
2   
3   import static org.hamcrest.CoreMatchers.*;
4   
5   import java.io.IOException;
6   import java.io.Serializable;
7   import java.text.NumberFormat;
8   import java.text.ParseException;
9   import java.util.HashMap;
10  import java.util.Map;
11  import java.util.regex.Matcher;
12  import java.util.regex.Pattern;
13  
14  import javax.persistence.DiscriminatorValue;
15  
16  import org.nakedobjects.metamodel.adapter.oid.Oid;
17  import org.nakedobjects.metamodel.adapter.oid.stringable.directly.DirectlyStringableOid;
18  import org.nakedobjects.metamodel.commons.encoding.DataInputExtended;
19  import org.nakedobjects.metamodel.commons.encoding.DataOutputExtended;
20  
21  import static org.nakedobjects.metamodel.commons.ensure.Ensure.*;
22  import org.nakedobjects.metamodel.spec.NakedObjectSpecification;
23  import org.nakedobjects.metamodel.specloader.NakedObjectReflector;
24  import org.nakedobjects.metamodel.util.ClassUtil;
25  import org.nakedobjects.runtime.context.NakedObjectsContext;
26  import org.nakedobjects.runtime.persistence.oidgenerator.OidGenerator;
27  import org.nakedobjects.runtime.persistence.services.ServiceUtil;
28  import org.starobjects.jpa.metamodel.facets.object.discriminator.JpaDiscriminatorValueFacet;
29  import org.starobjects.jpa.metamodel.facets.object.entity.JpaEntityFacet;
30  import org.starobjects.jpa.metamodel.specloader.DiscriminatorCache;
31  
32  
33  public final class JpaOid implements Oid, Cloneable, DirectlyStringableOid {
34  
35      private static final long serialVersionUID = 1L;
36  
37      private static final String DESTRING_IAE_EXCEPTION_MSG =
38              "Does not match format XXX|nnnn (persistent) or XXX|nnnn~ (transient)";
39      private static Pattern DESTRING_PATTERN = Pattern
40              .compile("^([A-Z]+)\\|([0-9]+)(~?)$");
41  
42      private static Map<Class<?>,String> discriminatorByClass = new HashMap<Class<?>,String>();
43  
44      private static String lookupDiscriminator(String className) {
45          try {
46              Class<?> cls = Thread.currentThread().getContextClassLoader().loadClass(className);
47              return lookupDiscriminator(cls);
48          } catch (ClassNotFoundException e) {
49              return className; // fallback
50          }
51      }
52  
53      private static String lookupDiscriminator(Class<?> cls) {
54          String discriminator = discriminatorByClass.get(cls);
55          if (discriminator == null) {
56              discriminator = lookupAndStoreDiscriminator(cls, discriminator);
57          }
58          if (discriminator != null) {
59              return discriminator;
60          }
61          // search up hierarchy
62          Class<?> superclass = cls.getSuperclass();
63          if (superclass == null) {
64              return cls.getName(); // fallback
65          }
66          return lookupDiscriminator(superclass); 
67      }
68  
69      private static synchronized String lookupAndStoreDiscriminator(Class<?> cls,
70              String discriminator) {
71          DiscriminatorValue discriminatorValue = cls.getAnnotation(DiscriminatorValue.class);
72          if (discriminatorValue == null) {
73              return null;
74          } 
75          discriminator = discriminatorValue.value();
76          discriminatorByClass.put(cls, discriminator);
77          return discriminator;
78      }
79      
80      public static void testReset() {
81          discriminatorByClass.clear();
82      }
83  
84      private String classname;
85      private Number id;
86      private Number newId;
87      private boolean isTransient;
88      private JpaOid previous;
89  
90      private String discriminator;
91  
92      private int cachedHashCode;
93      private String cachedToString;
94      private String cachedEnstring;
95  
96  
97      private static enum State {
98          TRANSIENT,
99          PERSISTENT;
100 
101         public boolean isTransient() {
102             return this == TRANSIENT;
103         }
104 
105         @SuppressWarnings("unused")
106         public boolean isPersistent() {
107             return this == PERSISTENT;
108         }
109 
110         public static State decode(final boolean isTransient) {
111             return isTransient ? TRANSIENT : PERSISTENT;
112         }
113     }
114 
115     // /////////////////////////////////////////////////////////
116     // Factory methods
117     // /////////////////////////////////////////////////////////
118 
119     /**
120      * Create a new transient instance using the provided <tt>id</tt>.
121      * 
122      * @see #createTransient(String, Number) for postconditions.
123      */
124     public static JpaOid createTransient(
125             final Class<?> clazz,
126             final Number id) {
127         return createTransient(clazz.getName(), id);
128     }
129 
130     /**
131      * Create a new transient id.
132      * <p>
133      * The {@link #getNewId()} will initially be <tt>null</tt>.
134      */
135     public static JpaOid createTransient(
136             final String className,
137             final Number id) {
138         return createTransient(className, id, null);
139     }
140 
141     /**
142      * Create a new transient id.
143      * <p>
144      * The {@link #getNewId()} will initially be <tt>null</tt>.
145      */
146     public static JpaOid createTransient(
147             final String className,
148             final Number id,
149             final String discriminator) {
150         return new JpaOid(className, id, null, State.TRANSIENT, discriminator);
151     }
152 
153     /**
154      * Creates a new persistent instance, using the specified <tt>id</tt>.
155      * 
156      * @see #createPersistent(String, Serializable) for postconditions.
157      */
158     public static JpaOid createPersistent(
159             final Class<?> clazz,
160             final Number id) {
161         return createPersistent(clazz.getName(), id);
162     }
163 
164     /**
165      * Creates a new persistent instance, using the specified <tt>id</tt>.
166      * <p>
167      * The {@link #getNewId()} will be the same as the <tt>primaryKey</tt>.
168      */
169     public static JpaOid createPersistent(
170             final String className,
171             final Number id) {
172         return createPersistent(className, id, null);
173     }
174 
175     /**
176      * Creates a new persistent instance, using the specified <tt>id</tt>.
177      * <p>
178      * The {@link #getNewId()} will be the same as the <tt>primaryKey</tt>.
179      */
180     public static JpaOid createPersistent(
181             final String className,
182             final Number id,
183             final String discriminator) {
184         return new JpaOid(className, id, null, State.PERSISTENT, discriminator);
185     }
186 
187     // /////////////////////////////////////////////////////////
188     // Constructor
189     // /////////////////////////////////////////////////////////
190 
191     /**
192      * If the <tt>discriminator</tt> is blank, then will lookup from classname.
193      */
194     private JpaOid(
195             final String classname,
196             final Number id,
197             final Number newId,
198             final State state,
199             final String discriminator) {
200         this.classname = classname;
201         this.id = id;
202         this.newId = newId;
203         this.isTransient = state.isTransient();
204         this.discriminator = lookupDiscriminatorIfRequired(discriminator);
205         initialized();
206     }
207 
208     private String lookupDiscriminatorIfRequired(final String discriminator) {
209         if (discriminator != null)
210             return discriminator;
211         else
212             return lookupDiscriminator(this.getClassName());
213     }
214 
215 
216     public JpaOid(final DataInputExtended input) throws IOException {
217         this.classname = input.readUTF();
218         this.id = input.readSerializable(Number.class);
219         this.isTransient = input.readBoolean();
220         this.newId = input.readSerializable(Number.class);
221         this.previous = input.readEncodable(JpaOid.class);
222         initialized();
223     }
224 
225     public void encode(final DataOutputExtended output) throws IOException {
226         output.writeUTF(classname);
227         output.writeSerializable(id);
228         output.writeBoolean(isTransient);
229         output.writeSerializable(newId);
230         output.writeEncodable(previous);
231     }
232 
233     private void initialized() {
234         cacheState();
235     }
236 
237     // /////////////////////////////////////////////////////////
238     // copyFrom
239     // /////////////////////////////////////////////////////////
240 
241     public void copyFrom(final Oid oid) {
242         ensureThatArg(oid instanceof JpaOid, is(true));
243         final JpaOid from = (JpaOid) oid;
244         this.id = from.id;
245         this.classname = from.classname;
246         this.newId = from.newId;
247         this.isTransient = from.isTransient;
248         cacheState();
249     }
250 
251     // /////////////////////////////////////////////////////////
252     // className, id, discriminator
253     // /////////////////////////////////////////////////////////
254 
255     /**
256      * Used in {@link #equals(Object)}.
257      */
258     public String getClassName() {
259         return classname;
260     }
261 
262     /**
263      * Used in {@link #equals(Object)}.
264      * <p>
265      * Will be the same as the {@link #getNewId()} once the Oid has been
266      * {@link #makePersistent() made persistent}.
267      * 
268      * @return
269      */
270     public Number getId() {
271         return id;
272     }
273     
274     public String getDiscriminator() {
275         return discriminator;
276     }
277     
278 
279     // /////////////////////////////////////////////////////////
280     // makePersistent, HibernateId
281     // /////////////////////////////////////////////////////////
282 
283     /**
284      * Use the {@link #getNewId() jpa Id} as the {@link #getId() primary key},
285      * in the
286      * process marking the Oid as {@link #isTransient() persistent}, and storing
287      * the {@link #getPrevious() previous} value.
288      * <p>
289      * Note 1: should be preceded by a call to {@link #setNewId(Serializable)}
290      * to set the {@link #getNewId() jpa Id}. The two methods cannot be combined
291      * because of {@link OidGenerator#convertPersistentToTransientOid(Oid)}. If
292      * this is not done then the id is left unchanged.
293      * <p>
294      * Note 2: if called then the {@link #hashCode()} may change; it is the
295      * caller's responsibility to manage any {@link Map}s that the Oid might be
296      * using.
297      */
298     public void makePersistent() {
299         ensureThatState(isTransient(), is(true));
300         this.previous = new JpaOid(this.classname, this.id, null,
301                 State.TRANSIENT, this.discriminator);
302 
303         if (newId != null) {
304             id = newId;
305         }
306         this.isTransient = false;
307         newId = null;
308         cacheState();
309     }
310 
311 
312     /**
313      * The id set by {@link #setNewId(Long)}.
314      * <p>
315      * Will return <tt>null</tt> once the Oid has been {@link #makePersistent()
316      * made persistent}.
317      */
318     public Number getNewId() {
319         return newId;
320     }
321 
322     /**
323      * Update the Id, prior to {@link #makePersistent()}.
324      * <p>
325      * The new Id does not come into effect until {@link #makePersistent()} is
326      * called.
327      */
328     public void setNewId(final Number newId) {
329         this.newId = newId;
330         cacheState();
331     }
332 
333 
334     // /////////////////////////////////////////////////////////
335     // getPrevious, hasPrevious
336     // /////////////////////////////////////////////////////////
337 
338     /**
339      * The previous Oid, if any.
340      * <p>
341      * Will hold this {@link Oid} in its transient form once
342      * {@link #makePersistent()} has been called. This allows client-side code
343      * to maintain its Oid->Object maps.
344      */
345     public Oid getPrevious() {
346         return previous;
347     }
348 
349     public boolean hasPrevious() {
350         return previous != null;
351     }
352 
353     public void clearPrevious() {
354         previous = null;
355     }
356 
357 
358     // /////////////////////////////////////////////////////////
359     // isTransient
360     // /////////////////////////////////////////////////////////
361 
362     public boolean isTransient() {
363         return isTransient;
364     }
365 
366 
367     // /////////////////////////////////////////////////////////
368     // DirectlyStringableOid
369     // /////////////////////////////////////////////////////////
370 
371 
372     public static JpaOid deString(final String oidStr) {
373         final NakedObjectReflector reflector =
374                 (NakedObjectReflector) NakedObjectsContext
375                 .getSpecificationLoader();
376         final DiscriminatorCache discriminatorCache =
377                 (DiscriminatorCache) reflector.getMetaModelValidator();
378         return deString(oidStr, discriminatorCache);
379     }
380 
381     public static JpaOid deString(final String oidStr,
382             final DiscriminatorCache discriminatorCache) {
383         if (oidStr == null) {
384             return null;
385         }
386         final Matcher matcher = DESTRING_PATTERN.matcher(oidStr);
387         if (!matcher.matches()) {
388             throw new IllegalArgumentException(DESTRING_IAE_EXCEPTION_MSG);
389         }
390         final String discriminator = matcher.group(1);
391         final NakedObjectSpecification noSpec = discriminatorCache
392                 .lookupClassFor(discriminator);
393         if (noSpec == null) {
394             throw new IllegalArgumentException("Unknown discriminator '" + discriminator + "'");
395         }
396         final String className = noSpec.getFullName();
397 
398         final String idStr = matcher.group(2);
399         Number id;
400         try {
401             id = NumberFormat.getInstance().parse(idStr);
402         } catch (final ParseException e) {
403             throw new IllegalArgumentException(DESTRING_IAE_EXCEPTION_MSG);
404         }
405 
406         final String transientStr = matcher.group(3);
407         return transientStr != null ? JpaOid.createTransient(className, id, discriminator)
408                 : JpaOid.createPersistent(className, id, discriminator);
409     }
410 
411     public String enString() {
412         return cachedEnstring;
413     }
414 
415 
416     // /////////////////////////////////////////////////////////
417     // equals, hashCode impl.
418     // /////////////////////////////////////////////////////////
419 
420     @Override
421     public boolean equals(final Object other) {
422         if (other == this) {
423             return true;
424         }
425         if (getClass() != other.getClass()) {
426             return false;
427         }
428         return equals((JpaOid) other);
429     }
430 
431     public boolean equals(final JpaOid other) {
432         return isTransient == other.isTransient &&
433                 classname.equals(other.classname) &&
434                 id.equals(other.id);
435     }
436 
437     /**
438      * Note that the hashCode changes when {@link #makePersistent() persisted}.
439      * Therefore
440      * the Oid must be removed from any maps prior to this.
441      */
442     @Override
443     public int hashCode() {
444         return cachedHashCode;
445     }
446 
447     private void cacheState() {
448         cachedHashCode = 17;
449         cachedHashCode = 37 * cachedHashCode + classname.hashCode();
450         cachedHashCode = 37 * cachedHashCode + id.hashCode();
451 
452         cachedToString = (isTransient() ? "T" : "") + "HOID#" + id.toString()
453                 + "/" + classname
454                 + (newId == null ? "" : "(" + newId + ")")
455                 + (previous == null ? "" : "+");
456 
457         cachedEnstring = discriminator + "|" + this.id + (isTransient?"~":"");
458     }
459 
460 
461     // /////////////////////////////////////////////////////////
462     // Cloneable
463     // /////////////////////////////////////////////////////////
464 
465     @Override
466     public JpaOid clone() {
467         final JpaOid jpaOid = new JpaOid(this.classname, this.id, this.newId,
468                 State.decode(this.isTransient), this.discriminator);
469         jpaOid.previous = this.previous;
470         return jpaOid;
471     }
472 
473 
474     // /////////////////////////////////////////////////////////
475     // toString
476     // /////////////////////////////////////////////////////////
477 
478     @Override
479     public String toString() {
480         return cachedToString;
481     }
482 
483 
484 }