View Javadoc

1   /**
2    * 2008, Digitalis Informatica. All rights reserved. Distribuicao e Gestao de Informatica, Lda. Estrada de Paco de Arcos
3    * num.9 - Piso -1 2780-666 Paco de Arcos Telefone: (351) 21 4408990 Fax: (351) 21 4408999 http://www.digitalis.pt
4    */
5   
6   package pt.digitalis.dif.model.dataset;
7   
8   import java.beans.PropertyDescriptor;
9   import java.util.LinkedHashMap;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.Map.Entry;
13  import java.util.Set;
14  
15  import org.apache.commons.beanutils.PropertyUtils;
16  
17  import pt.digitalis.utils.common.IBeanAttributes;
18  import pt.digitalis.utils.common.StringUtils;
19  import pt.digitalis.utils.common.collections.CaseInsensitiveHashMap;
20  import pt.digitalis.utils.common.collections.CaseInsentiveArrayList;
21  
22  /**
23   * An abstract base implementation for data sources
24   * 
25   * @author Pedro Viegas <a href="mailto:pviegas@digitalis.pt">pviegas@digitalis.pt</a><br/>
26   * @param <T>
27   *            the data object type of the data source
28   * @created 2008/11/19
29   */
30  public abstract class AbstractDataSet<T extends IBeanAttributes> implements IDataSet<T> {
31  
32      /**
33       * Throws an unsupported operation exception for non-implemented dataset operations
34       * 
35       * @param reason
36       *            the reason why this operation in not supported
37       * @throws DataSetException
38       */
39      static public void throwUnsuportedOperationException(String reason) throws DataSetException
40      {
41          StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
42  
43          throw new DataSetException(reason, new RuntimeException("Unsuported Dataset Operation: "
44                  + stackTraceElements[1].getClassName() + stackTraceElements[1].getMethodName() + "\n" + reason));
45      }
46  
47      /** The attributes definition objects */
48      protected CaseInsensitiveHashMap<AttributeDefinition> attributesDefinition;
49  
50      /** the changes recorder */
51      private LinkedHashMap<String, ChangeDescriptor<T>> changeSet = new LinkedHashMap<String, ChangeDescriptor<T>>();
52  
53      /** The data object class */
54      protected Class<T> clazz;
55  
56      /** The attribute to use as the data object id */
57      protected String idAttribute = "id";
58  
59      /** a possible ID generator to use for inserts */
60      private IIDGenerator<T> idGenerator;
61  
62      /** if T will not report development errors */
63      boolean ignoreDevelopmentErrors = false;
64  
65      /** if T will track all changes made to the dataset from this point on */
66      private boolean trackChanges = false;
67  
68      /**
69       * Default constructor
70       * 
71       * @param clazz
72       */
73      public AbstractDataSet(Class<T> clazz)
74      {
75          this.clazz = clazz;
76      }
77  
78      /**
79       * @see pt.digitalis.dif.model.dataset.IDataSet#delete(java.lang.String)
80       */
81      final public boolean delete(String id) throws DataSetException
82      {
83          if (this.isTrackChanges())
84          {
85              T instance = this.get(id);
86  
87              // Only if the id is in fact in the current dataset
88              if (instance != null)
89              {
90                  // See if previous changes exist for this record
91                  ChangeDescriptor<T> change = this.getInternalChangeSet().get(id);
92  
93                  if (change == null)
94                  {
95                      // No previous changes. Add the deletion.
96                      change = new ChangeDescriptor<T>(DMLOperation.DELETE, RecordType.ORIGINAL, instance);
97                      this.getInternalChangeSet().put(id, change);
98                  }
99                  else
100                 {
101                     // Previous changes exist. Keep previous record type (could be NEW for inserted or UPDATED for
102                     // changed record
103                     change = new ChangeDescriptor<T>(DMLOperation.DELETE, change.getRecordType(), instance);
104                     this.getInternalChangeSet().put(id, change);
105                 }
106             }
107         }
108 
109         return this.deleteSpecific(id);
110     }
111 
112     /**
113      * Deletes a given data object
114      * 
115      * @param id
116      *            the id of the object to delete
117      * @return T if the update was successful
118      * @throws DataSetException
119      */
120     abstract public boolean deleteSpecific(String id) throws DataSetException;
121 
122     /**
123      * Detects the current data object attributes
124      */
125     protected void detectAttributeDefinition()
126     {
127         if (clazz != null)
128         {
129             PropertyDescriptor[] propDescriptors = PropertyUtils.getPropertyDescriptors(clazz);
130             CaseInsensitiveHashMap<AttributeDefinition> tempAttributesDef = new CaseInsensitiveHashMap<AttributeDefinition>();
131 
132             for (PropertyDescriptor propDescriptor: propDescriptors)
133             {
134 
135                 if (!propDescriptor.getName().equals("class") && !propDescriptor.getName().equals("bytes")
136                         && (propDescriptor.getPropertyType() != Map.class)
137                         && (propDescriptor.getPropertyType() != List.class)
138                         && (propDescriptor.getPropertyType() != Set.class))
139                 {
140                     AttributeDefinition def = new AttributeDefinition(propDescriptor.getName(),
141                             StringUtils.camelCaseToString(propDescriptor.getName()), propDescriptor.getPropertyType());
142 
143                     tempAttributesDef.put(propDescriptor.getName(), def);
144                 }
145             }
146 
147             attributesDefinition = tempAttributesDef;
148         }
149     }
150 
151     /**
152      * Executed a given query.
153      * 
154      * @param query
155      *            the query object to execute
156      * @return the list of business instances
157      * @throws DataSetException
158      *             the data set exception
159      */
160     @SuppressWarnings("unchecked")
161     public List<T> executeQuery(Query<T> query) throws DataSetException
162     {
163         CollectorListProcessor processor = new CollectorListProcessor();
164         this.executeQuery(query, processor);
165 
166         return (List<T>) processor.getList();
167     }
168 
169     /**
170      * @see pt.digitalis.dif.model.dataset.IDataSet#getAttributeDefinition(java.lang.String)
171      */
172     public AttributeDefinition getAttributeDefinition(String attribute)
173     {
174         return this.getAttributesDefinition().get(attribute);
175     }
176 
177     /**
178      * @see pt.digitalis.dif.model.dataset.IDataSet#getAttributeList()
179      */
180     public CaseInsentiveArrayList getAttributeList()
181     {
182         return new CaseInsentiveArrayList(getAttributesDefinition().keySet());
183     }
184 
185     /**
186      * @see pt.digitalis.dif.model.dataset.IDataSet#getAttributesDefinition()
187      */
188     public CaseInsensitiveHashMap<AttributeDefinition> getAttributesDefinition()
189     {
190 
191         if (attributesDefinition == null)
192             detectAttributeDefinition();
193 
194         return attributesDefinition;
195     }
196 
197     /**
198      * @see pt.digitalis.dif.model.dataset.IDataSet#getChanges()
199      */
200     public Map<String, ChangeDescriptor<T>> getChanges() throws DataSetException
201     {
202         LinkedHashMap<String, ChangeDescriptor<T>> result = new LinkedHashMap<String, ChangeDescriptor<T>>();
203 
204         for (Entry<String, ChangeDescriptor<T>> change: this.getInternalChangeSet().entrySet())
205         {
206             ChangeDescriptor<T> internalChangeDescriptor = change.getValue();
207 
208             if (internalChangeDescriptor.getOperation() == DMLOperation.DELETE
209                     && internalChangeDescriptor.getRecordType() == RecordType.NEW)
210                 // A record that was inserted and deleted afterwards.
211                 // No need to report, the end result is the same as in the beginning
212                 ;
213             else
214             {
215                 T bean = internalChangeDescriptor.getBeanInstance();
216                 if (bean == null)
217                     bean = this.get(change.getKey());
218 
219                 ChangeDescriptor<T> newChangeDescriptor = new ChangeDescriptor<T>(
220                         internalChangeDescriptor.getOperation(), internalChangeDescriptor.getRecordType(), bean);
221 
222                 result.put(change.getKey(), newChangeDescriptor);
223             }
224         }
225 
226         return result;
227     }
228 
229     /**
230      * @see pt.digitalis.dif.model.dataset.IDataSet#getIDFieldName()
231      */
232     public String getIDFieldName()
233     {
234         return idAttribute;
235     }
236 
237     /**
238      * Inspector for the 'idGenerator' attribute.
239      * 
240      * @return the idGenerator value
241      */
242     public IIDGenerator<T> getIdGenerator()
243     {
244         return idGenerator;
245     }
246 
247     /**
248      * Inspector for the 'changeSet' attribute.
249      * 
250      * @return the changeSet value
251      */
252     protected Map<String, ChangeDescriptor<T>> getInternalChangeSet()
253     {
254         return changeSet;
255     }
256 
257     /**
258      * @see pt.digitalis.dif.model.dataset.IDataSet#insert(java.lang.String, java.util.Map)
259      */
260     public T insert(String id, Map<String, String> attributeValues) throws DataSetException
261     {
262 
263         T instance = instanciateDataObject(attributeValues);
264         if (id != null)
265         {
266             instance.setAttributeFromString(idAttribute, id);
267         }
268 
269         return insert(instance);
270     }
271 
272     /**
273      * @see pt.digitalis.dif.model.dataset.IDataSet#insert(pt.digitalis.utils.common.IBeanAttributes)
274      */
275     final public T insert(T instance) throws DataSetException
276     {
277         // No ID passed and an ID generator exists. Use it!
278         if (this.getIdGenerator() != null && instance.getAttribute(this.getIDFieldName()) == null)
279         {
280             String newID = this.getIdGenerator().generateID(instance);
281             instance.setAttributeFromString(this.getIDFieldName(), newID);
282         }
283 
284         instance = this.insertSpecific(instance);
285 
286         if (this.getIdGenerator() != null)
287             this.getIdGenerator().reportInsertedID(instance.getAttributeAsString(this.getIDFieldName()));
288 
289         // TrackChanges
290         if (this.isTrackChanges())
291         {
292             if (instance.getAttribute(this.getIDFieldName()) == null)
293                 throwUnsuportedOperationException("Cannot use track changes for empty ID inserts. "
294                         + "Set the ID field before inserting the record in the dataset.");
295             else
296             {
297                 ChangeDescriptor<T> change = this.getInternalChangeSet().get(
298                         instance.getAttributeAsString(this.getIDFieldName()));
299 
300                 if (change == null)
301                 {
302                     // New record
303                     change = new ChangeDescriptor<T>(DMLOperation.INSERT, RecordType.NEW, null);
304                     this.getInternalChangeSet().put(instance.getAttributeAsString(this.getIDFieldName()), change);
305                 }
306                 else
307                 {
308                     // Record has already existed
309                     if (change.getRecordType() == RecordType.NEW)
310                         // New record, keep previous change set but with INSERT instead of delete
311                         change = new ChangeDescriptor<T>(DMLOperation.INSERT, RecordType.NEW, null);
312                     else
313                     {
314                         // Not new, then a possible delete that has now been restored, or previous update that will be
315                         // overridden by this insert.
316                         change = new ChangeDescriptor<T>(DMLOperation.UPDATE, RecordType.UPDATED, null);
317                     }
318 
319                     this.getInternalChangeSet().put(instance.getAttributeAsString(this.getIDFieldName()), change);
320                 }
321             }
322         }
323 
324         return instance;
325     }
326 
327     /**
328      * Inserts a new data object
329      * 
330      * @param instance
331      *            the data object to insert, with the new values
332      * @return the inserted data object
333      * @throws DataSetException
334      */
335     abstract public T insertSpecific(T instance) throws DataSetException;
336 
337     /**
338      * @return an instantiated generic data object
339      */
340     protected T instanciateDataObject()
341     {
342         if (clazz != null)
343         {
344             try
345             {
346                 T instance = clazz.newInstance();
347 
348                 return instance;
349             }
350             catch (InstantiationException e)
351             {
352                 return null;
353             }
354             catch (IllegalAccessException e)
355             {
356                 return null;
357             }
358         }
359         return null;
360     }
361 
362     /**
363      * @param attributeValues
364      *            the values to set the instance attributes with
365      * @return an instantiated generic data object
366      */
367     protected T instanciateDataObject(Map<String, String> attributeValues)
368     {
369         T instance = instanciateDataObject();
370 
371         if (instance != null)
372             for (Entry<String, String> entry: attributeValues.entrySet())
373                 instance.setAttributeFromString(entry.getKey(), entry.getValue());
374 
375         return instance;
376     }
377 
378     /**
379      * @see pt.digitalis.dif.model.dataset.IDataSet#isCompositeID()
380      */
381     public boolean isCompositeID()
382     {
383         return false;
384     }
385 
386     /**
387      * @see pt.digitalis.dif.model.dataset.IDataSet#isIgnoreDevelopmentErrors()
388      */
389     public boolean isIgnoreDevelopmentErrors()
390     {
391         return ignoreDevelopmentErrors;
392     }
393 
394     /**
395      * @see pt.digitalis.dif.model.dataset.IDataSet#isTrackChanges()
396      */
397     public boolean isTrackChanges()
398     {
399         return trackChanges;
400     }
401 
402     /**
403      * @see pt.digitalis.dif.model.dataset.IDataSet#newDataInstance()
404      */
405     public T newDataInstance()
406     {
407         return instanciateDataObject();
408     }
409 
410     /**
411      * @see pt.digitalis.dif.model.dataset.IDataSet#query()
412      */
413     public Query<T> query()
414     {
415         return new Query<T>(this, true, true);
416     }
417 
418     /**
419      * @see pt.digitalis.dif.model.dataset.IDataSet#refresh(pt.digitalis.utils.common.IBeanAttributes)
420      */
421     public T refresh(T instance) throws DataSetException
422     {
423         return instance;
424     }
425 
426     /**
427      * @see pt.digitalis.dif.model.dataset.IDataSet#resetTrackChanges()
428      */
429     public void resetTrackChanges()
430     {
431         this.getInternalChangeSet().clear();
432     }
433 
434     /**
435      * Modifier for the 'idGenerator' attribute.
436      * 
437      * @param idGenerator
438      *            the new idGenerator value to set
439      * @throws DataSetException
440      */
441     public void setIdGenerator(IIDGenerator<T> idGenerator) throws DataSetException
442     {
443         if (this.isCompositeID())
444             throwUnsuportedOperationException("Can only use ID Generator for non-composite ID datasets");
445         else
446             this.idGenerator = idGenerator;
447     }
448 
449     /**
450      * @see pt.digitalis.dif.model.dataset.IDataSet#setIgnoreDevelopmentErrors(boolean)
451      */
452     public void setIgnoreDevelopmentErrors(boolean ignoreDevelopmentErrors)
453     {
454         this.ignoreDevelopmentErrors = ignoreDevelopmentErrors;
455     }
456 
457     /**
458      * @see pt.digitalis.dif.model.dataset.IDataSet#setTrackChanges(boolean)
459      */
460     public void setTrackChanges(boolean trackChanges)
461     {
462         this.trackChanges = trackChanges;
463 
464         if (trackChanges)
465             changeSet = new LinkedHashMap<String, ChangeDescriptor<T>>();
466     }
467 
468     /**
469      * @see pt.digitalis.dif.model.dataset.IDataSet#undelete(java.lang.String)
470      */
471     public T undelete(String id) throws DataSetException
472     {
473         if (!this.isTrackChanges())
474         {
475             throwUnsuportedOperationException("Cannot undelete a record without track changes active");
476 
477             // Does not reach here. Just for Eclipse complication errors sake!
478             return null;
479         }
480         else
481         {
482             ChangeDescriptor<T> change = this.getInternalChangeSet().get(id);
483 
484             if (change == null)
485             {
486                 String messageRecordId = this.clazz.getSimpleName() + "[" + id + "]";
487                 throw new DataSetException("Record not found for recover: " + messageRecordId);
488             }
489             else
490             {
491                 // Reinsert it in the database
492                 T instance = this.insertSpecific(change.getBeanInstance());
493 
494                 if (change.getRecordType() == RecordType.NEW)
495                 {
496                     // New record, keep previous change set but with INSERT instead of delete
497                     change = new ChangeDescriptor<T>(DMLOperation.INSERT, RecordType.NEW, null);
498                 }
499                 else if (change.getRecordType() == RecordType.UPDATED)
500                 {
501                     // Previously updated record, keep previous change set but with UPDATE instead of delete
502                     change = new ChangeDescriptor<T>(DMLOperation.UPDATE, RecordType.UPDATED, null);
503                 }
504                 else
505                 {
506                     // Original record deleted.
507                     // Undelete replaced the original data so, the change became irrelevant. Remove it.
508                     change = null;
509                 }
510 
511                 if (change == null)
512                     this.getInternalChangeSet().remove(id);
513                 else
514                     this.getInternalChangeSet().put(id, change);
515 
516                 return instance;
517             }
518         }
519     }
520 
521     /**
522      * @see pt.digitalis.dif.model.dataset.IDataSet#update(java.lang.String, java.util.Map)
523      */
524     public T update(String id, Map<String, String> attributeValues) throws DataSetException
525     {
526 
527         T instance = get(id);
528 
529         if (instance != null)
530         {
531             for (Entry<String, String> prop: attributeValues.entrySet())
532             {
533 
534                 if (prop.getValue() == null)
535                     instance.setAttribute(prop.getKey(), null);
536                 else if (!(instance.getAttribute(prop.getKey()) instanceof String) && "".equals(prop.getValue()))
537                     instance.setAttribute(prop.getKey(), null);
538                 else
539                     instance.setAttributeFromString(prop.getKey(), prop.getValue());
540             }
541             return update(instance);
542         }
543         else
544             return null;
545     }
546 
547     /**
548      * @see pt.digitalis.dif.model.dataset.IDataSet#update(pt.digitalis.utils.common.IBeanAttributes)
549      */
550     final public T update(T instance) throws DataSetException
551     {
552         instance = this.updateSpecific(instance);
553 
554         if (this.isTrackChanges() && this.get(instance.getAttributeAsString(this.getIDFieldName())) != null)
555         {
556             // Only if the id is in fact in the current dataset
557             // See if previous changes exist for this record
558             String id = instance.getAttributeAsString(this.getIDFieldName());
559             ChangeDescriptor<T> change = this.getInternalChangeSet().get(id);
560 
561             if (change == null)
562             {
563                 // No previous changes. Add the update.
564                 change = new ChangeDescriptor<T>(DMLOperation.UPDATE, RecordType.UPDATED, null);
565                 this.getInternalChangeSet().put(id, change);
566             }
567             else
568             {
569                 // Previous changes exist. If it's a NEW record, keep it, since changes to a non persisted record are
570                 // translated to an insert anyway
571                 if (change.getRecordType() == RecordType.NEW)
572                     change = new ChangeDescriptor<T>(DMLOperation.INSERT, RecordType.NEW, null);
573                 else
574                     change = new ChangeDescriptor<T>(DMLOperation.UPDATE, RecordType.UPDATED, null);
575 
576                 this.getInternalChangeSet().put(id, change);
577             }
578         }
579 
580         return instance;
581     }
582 
583     /**
584      * @see pt.digitalis.dif.model.dataset.IDataSet#updateBean(pt.digitalis.utils.common.IBeanAttributes)
585      */
586     public T updateBean(T instance) throws DataSetException
587     {
588         return update(instance);
589     }
590 
591     /**
592      * Updates a given data object
593      * 
594      * @param instance
595      *            the data object to update, with the new values
596      * @return the updated data object
597      * @throws DataSetException
598      */
599     abstract public T updateSpecific(T instance) throws DataSetException;
600 
601 }