View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.data.jpa.eclipselink;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.eclipse.persistence.config.SessionCustomizer;
20  import org.eclipse.persistence.descriptors.ClassDescriptor;
21  import org.eclipse.persistence.exceptions.DescriptorException;
22  import org.eclipse.persistence.internal.databaseaccess.Accessor;
23  import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
24  import org.eclipse.persistence.internal.sessions.AbstractSession;
25  import org.eclipse.persistence.mappings.DatabaseMapping;
26  import org.eclipse.persistence.sequencing.Sequence;
27  import org.eclipse.persistence.sessions.DatabaseLogin;
28  import org.eclipse.persistence.sessions.JNDIConnector;
29  import org.eclipse.persistence.sessions.Session;
30  import org.kuali.rice.krad.data.jpa.DisableVersioning;
31  import org.kuali.rice.krad.data.jpa.Filter;
32  import org.kuali.rice.krad.data.jpa.FilterGenerator;
33  import org.kuali.rice.krad.data.jpa.FilterGenerators;
34  import org.kuali.rice.krad.data.jpa.PortableSequenceGenerator;
35  import org.kuali.rice.krad.data.jpa.RemoveMapping;
36  import org.kuali.rice.krad.data.jpa.RemoveMappings;
37  import org.kuali.rice.krad.data.platform.MaxValueIncrementerFactory;
38  import org.springframework.core.annotation.AnnotationUtils;
39  import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
40  
41  import javax.sql.DataSource;
42  import java.lang.reflect.Field;
43  import java.lang.reflect.Method;
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.List;
47  import java.util.Map;
48  import java.util.Vector;
49  import java.util.concurrent.ConcurrentHashMap;
50  import java.util.concurrent.ConcurrentMap;
51  
52  /**
53   * EclipseLink Session Customizer which understands {@link org.kuali.rice.krad.data.jpa.PortableSequenceGenerator}
54   * annotations and automatically registers custom EclipseLink Sequences.
55   *
56   * <p>
57   * Since SessionCustomizers are stateless instances, and because concrete
58   * {@link org.eclipse.persistence.sequencing.Sequence} objects must be registered individually with the EclipseLink
59   * session, we lazy generate the Sequence objects using annotation inspection and then register them on each new
60   * session using this customizer.
61   * </p>
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   */
65  public class KradEclipseLinkCustomizer implements SessionCustomizer {
66  
67      private static ConcurrentMap<String, List<Sequence>> sequenceMap = new ConcurrentHashMap<String, List<Sequence>>(8,
68              0.9f, 1);
69  
70      /* Keyed by the session name determines if the class descriptors have been modified for the current session. */
71      private static ConcurrentMap<String, Boolean> modDescMap = new ConcurrentHashMap<String, Boolean>();
72  
73      private static ConcurrentMap<String, List<FilterGenerator>> queryCustomizerMap =
74              new ConcurrentHashMap<String, List<FilterGenerator>>();
75  
76      /**
77       * {@inheritDoc}
78       */
79      @Override
80      public void customize(Session session) throws Exception {
81          String sessionName = session.getName();
82  
83          // double-checked locking on ConcurrentMap
84          List<Sequence> sequences = sequenceMap.get(sessionName);
85          if (sequences == null) {
86              sequences = sequenceMap.putIfAbsent(sessionName, loadSequences(session));
87              if (sequences == null) {
88                  sequences = sequenceMap.get(sessionName);
89              }
90          }
91  
92          loadQueryCustomizers(session);
93  
94          DatabaseLogin login = session.getLogin();
95          for (Sequence sequence : sequences) {
96              login.addSequence(sequence);
97          }
98  
99          handleDescriptorModifications(session);
100 
101     }
102 
103     /**
104      * Load Query Customizer based on annotations on fields and call customizer to modify descriptor.
105      *
106      * @param session the EclipseLink session.
107      */
108     protected void loadQueryCustomizers(Session session) {
109         Map<Class, ClassDescriptor> descriptors = session.getDescriptors();
110         for (Class<?> entityClass : descriptors.keySet()) {
111             for (Field field : entityClass.getDeclaredFields()) {
112                 String queryCustEntry = entityClass.getName() + "_" + field.getName();
113                 buildQueryCustomizers(entityClass,field,queryCustEntry);
114 
115                 List<FilterGenerator> queryCustomizers = queryCustomizerMap.get(queryCustEntry);
116                 if (queryCustomizers != null && !queryCustomizers.isEmpty()) {
117                     Filter.customizeField(queryCustomizers, descriptors.get(entityClass), field.getName());
118                 }
119             }
120         }
121 
122     }
123 
124     /**
125      * Build and populate map of QueryCustomizer annotations.
126      *
127      * @param entityClass the type of the entity.
128      * @param field the field to process.
129      * @param key the id to store the customizer under.
130      */
131     protected void buildQueryCustomizers(Class<?> entityClass,Field field, String key){
132         FilterGenerators customizers = field.getAnnotation(FilterGenerators.class);
133         List<FilterGenerator> filterGenerators = new ArrayList<FilterGenerator>();
134         if(customizers != null){
135             filterGenerators.addAll(Arrays.asList(customizers.value()));
136         } else {
137             FilterGenerator customizer = field.getAnnotation(FilterGenerator.class);
138             if(customizer != null){
139                 filterGenerators.add(customizer);
140             }
141         }
142         for(FilterGenerator customizer : filterGenerators){
143             List<FilterGenerator> filterCustomizers = queryCustomizerMap.get(key);
144             if (filterCustomizers == null) {
145                 filterCustomizers =
146                         new ArrayList<FilterGenerator>();
147                 filterCustomizers.add(customizer);
148                 queryCustomizerMap.putIfAbsent(key, filterCustomizers);
149             } else {
150                 filterCustomizers.add(customizer);
151                 queryCustomizerMap.put(key,filterCustomizers);
152             }
153         }
154     }
155 
156     /**
157      * Determines if the class descriptors have been modified for the given session name.
158      *
159      * @param session the current session.
160      */
161     protected void handleDescriptorModifications(Session session) {
162         String sessionName = session.getName();
163 
164         // double-checked locking on ConcurrentMap
165         Boolean descModified = modDescMap.get(sessionName);
166         if (descModified == null) {
167             descModified = modDescMap.putIfAbsent(sessionName, Boolean.FALSE);
168             if (descModified == null) {
169                 descModified = modDescMap.get(sessionName);
170             }
171         }
172 
173         if (Boolean.FALSE.equals(descModified)) {
174             modDescMap.put(sessionName, Boolean.TRUE);
175             handleDisableVersioning(session);
176             handleRemoveMapping(session);
177         }
178     }
179 
180     /**
181      * Checks class descriptors for {@link @DisableVersioning} annotations at the class level and removes the version
182      * database mapping for optimistic locking.
183      *
184      * @param session the current session.
185      */
186     protected void handleDisableVersioning(Session session) {
187         Map<Class, ClassDescriptor> descriptors = session.getDescriptors();
188 
189         if (descriptors == null || descriptors.isEmpty()) {
190             return;
191         }
192 
193         for (ClassDescriptor classDescriptor : descriptors.values()) {
194             if (classDescriptor != null && AnnotationUtils.findAnnotation(classDescriptor.getJavaClass(),
195                     DisableVersioning.class) != null) {
196                 OptimisticLockingPolicy olPolicy = classDescriptor.getOptimisticLockingPolicy();
197                 if (olPolicy != null) {
198                     classDescriptor.setOptimisticLockingPolicy(null);
199                 }
200             }
201         }
202     }
203 
204     /**
205      * Checks class descriptors for {@link @RemoveMapping} and {@link RemoveMappings} annotations at the class level
206      * and removes any specified mappings from the ClassDescriptor.
207      *
208      * @param session the current session.
209      */
210     protected void handleRemoveMapping(Session session) {
211         Map<Class, ClassDescriptor> descriptors = session.getDescriptors();
212 
213         if (descriptors == null || descriptors.isEmpty()) {
214             return;
215         }
216 
217         for (ClassDescriptor classDescriptor : descriptors.values()) {
218             List<RemoveMapping> removeMappings = scanForRemoveMappings(classDescriptor);
219             if (!removeMappings.isEmpty()) {
220                 List<DatabaseMapping> mappingsToRemove = new ArrayList<DatabaseMapping>();
221                 for (RemoveMapping removeMapping : removeMappings) {
222                     if (StringUtils.isBlank(removeMapping.name())) {
223                         throw DescriptorException.attributeNameNotSpecified();
224                     }
225                     DatabaseMapping databaseMapping = classDescriptor.getMappingForAttributeName(removeMapping.name());
226                     if (databaseMapping == null) {
227                         throw DescriptorException.mappingForAttributeIsMissing(removeMapping.name(), classDescriptor);
228                     }
229                     mappingsToRemove.add(databaseMapping);
230                 }
231                 for (DatabaseMapping mappingToRemove : mappingsToRemove) {
232                     classDescriptor.removeMappingForAttributeName(mappingToRemove.getAttributeName());
233                 }
234             }
235         }
236     }
237 
238     /**
239      * Gets any {@link RemoveMapping}s out of the given {@link ClassDescriptor}.
240      *
241      * @param classDescriptor the {@link ClassDescriptor} to scan.
242      * @return a list of {@link RemoveMapping}s from the given {@link ClassDescriptor}.
243      */
244     protected List<RemoveMapping> scanForRemoveMappings(ClassDescriptor classDescriptor) {
245         List<RemoveMapping> removeMappings = new ArrayList<RemoveMapping>();
246         RemoveMappings removeMappingsAnnotation = AnnotationUtils.findAnnotation(classDescriptor.getJavaClass(),
247                 RemoveMappings.class);
248         if (removeMappingsAnnotation == null) {
249             RemoveMapping removeMappingAnnotation = AnnotationUtils.findAnnotation(classDescriptor.getJavaClass(),
250                     RemoveMapping.class);
251             if (removeMappingAnnotation != null) {
252                 removeMappings.add(removeMappingAnnotation);
253             }
254         } else {
255             for (RemoveMapping removeMapping : removeMappingsAnnotation.value()) {
256                 removeMappings.add(removeMapping);
257             }
258         }
259         return removeMappings;
260     }
261 
262     /**
263      * Gets any {@link Sequence} from the session.
264      *
265      * @param session the current session.
266      * @return a list of {@link Sequence}s.
267      */
268     protected List<Sequence> loadSequences(Session session) {
269         Map<Class, ClassDescriptor> descriptors = session.getDescriptors();
270         List<PortableSequenceGenerator> sequenceGenerators = new ArrayList<PortableSequenceGenerator>();
271         for (Class<?> entityClass : descriptors.keySet()) {
272             PortableSequenceGenerator sequenceGenerator = AnnotationUtils.findAnnotation(entityClass,
273                     PortableSequenceGenerator.class);
274             if (sequenceGenerator != null) {
275                 sequenceGenerators.add(sequenceGenerator);
276             }
277             loadFieldSequences(entityClass, sequenceGenerators);
278             for (Method method : entityClass.getMethods()) {
279                 PortableSequenceGenerator methodSequenceGenerator = method.getAnnotation(
280                         PortableSequenceGenerator.class);
281                 if (methodSequenceGenerator != null) {
282                     sequenceGenerators.add(methodSequenceGenerator);
283                 }
284             }
285         }
286         List<Sequence> sequences = new ArrayList<Sequence>();
287         for (PortableSequenceGenerator sequenceGenerator : sequenceGenerators) {
288             Sequence sequence = new MaxValueIncrementerSequenceWrapper(sequenceGenerator);
289             sequences.add(sequence);
290         }
291         return sequences;
292     }
293 
294     /**
295      * Loads any field-based sequences from the given type.
296      *
297      * @param entityClass the type of the entity.
298      * @param sequenceGenerators the current list of sequence generators.
299      */
300     protected void loadFieldSequences(Class<?> entityClass, List<PortableSequenceGenerator> sequenceGenerators) {
301         for (Field field : entityClass.getDeclaredFields()) {
302             PortableSequenceGenerator fieldSequenceGenerator = field.getAnnotation(PortableSequenceGenerator.class);
303             if (fieldSequenceGenerator != null) {
304                 sequenceGenerators.add(fieldSequenceGenerator);
305             }
306         }
307         // next, walk up and check the super class...
308         if (entityClass.getSuperclass() != null) {
309             loadFieldSequences(entityClass.getSuperclass(), sequenceGenerators);
310         }
311     }
312 
313     /**
314      * Translates our {@link PortableSequenceGenerator} into an EclipseLink {@link Sequence}.
315      */
316     private static final class MaxValueIncrementerSequenceWrapper extends Sequence {
317 
318         private static final long serialVersionUID = 2375805962996574386L;
319 
320         private final String sequenceName;
321 
322         /**
323          * Creates a sequence wrapper for our {@link PortableSequenceGenerator}.
324          *
325          * @param sequenceGenerator the {@link PortableSequenceGenerator} to process.
326          */
327         MaxValueIncrementerSequenceWrapper(PortableSequenceGenerator sequenceGenerator) {
328             super(sequenceGenerator.name(), 0);
329             // default sequenceName to the name of the sequence generator if the sequence name was not provided
330             if (StringUtils.isBlank(sequenceGenerator.sequenceName())) {
331                 sequenceName = sequenceGenerator.name();
332             } else {
333                 sequenceName = sequenceGenerator.sequenceName();
334             }
335         }
336 
337         /**
338          * {@inheritDoc}
339          */
340         @Override
341         public boolean shouldAcquireValueAfterInsert() {
342             return false;
343         }
344 
345         /**
346          * {@inheritDoc}
347          */
348         @Override
349         public boolean shouldUseTransaction() {
350             return true;
351         }
352 
353         /**
354          * {@inheritDoc}
355          */
356         @Override
357         public boolean shouldUsePreallocation() {
358             return false;
359         }
360 
361         /**
362          * {@inheritDoc}
363          */
364         @Override
365         public Object getGeneratedValue(Accessor accessor, AbstractSession writeSession, String seqName) {
366             DataSource dataSource = ((JNDIConnector) writeSession.getLogin().getConnector()).getDataSource();
367             DataFieldMaxValueIncrementer incrementer = MaxValueIncrementerFactory.getIncrementer(dataSource,
368                     sequenceName);
369             return Long.valueOf(incrementer.nextLongValue());
370         }
371 
372         /**
373          * {@inheritDoc}
374          */
375         @Override
376         public Vector<?> getGeneratedVector(Accessor accessor, AbstractSession writeSession, String seqName, int size) {
377             // we're not in the business of pre-fetching/allocating ids
378             throw new UnsupportedOperationException(getClass().getName() + " does pre-generate sequence ids");
379         }
380 
381         /**
382          * {@inheritDoc}
383          */
384         @Override
385         public void onConnect() {}
386 
387         /**
388          * {@inheritDoc}
389          */
390         @Override
391         public void onDisconnect() {}
392 
393         /**
394          * {@inheritDoc}
395          */
396         @Override
397         public MaxValueIncrementerSequenceWrapper clone() {
398             return (MaxValueIncrementerSequenceWrapper) super.clone();
399         }
400 
401     }
402 
403 }