View Javadoc
1   /**
2    * Copyright 2005-2015 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<DatabaseMapping> mappingsToRemove = new ArrayList<DatabaseMapping>();
219             List<RemoveMapping> removeMappings = scanForRemoveMappings(classDescriptor);
220 
221             for (RemoveMapping removeMapping : removeMappings) {
222                 if (StringUtils.isBlank(removeMapping.name())) {
223                     throw DescriptorException.attributeNameNotSpecified();
224                 }
225 
226                 DatabaseMapping databaseMapping = classDescriptor.getMappingForAttributeName(removeMapping.name());
227 
228                 if (databaseMapping == null) {
229                     throw DescriptorException.mappingForAttributeIsMissing(removeMapping.name(), classDescriptor);
230                 }
231 
232                 mappingsToRemove.add(databaseMapping);
233             }
234 
235             for (DatabaseMapping mappingToRemove : mappingsToRemove) {
236                 classDescriptor.removeMappingForAttributeName(mappingToRemove.getAttributeName());
237             }
238         }
239     }
240 
241     /**
242      * Gets any {@link RemoveMapping}s out of the given {@link ClassDescriptor}.
243      *
244      * @param classDescriptor the {@link ClassDescriptor} to scan.
245      * @return a list of {@link RemoveMapping}s from the given {@link ClassDescriptor}.
246      */
247     protected List<RemoveMapping> scanForRemoveMappings(ClassDescriptor classDescriptor) {
248         List<RemoveMapping> removeMappings = new ArrayList<RemoveMapping>();
249         RemoveMappings removeMappingsAnnotation = AnnotationUtils.findAnnotation(classDescriptor.getJavaClass(),
250                 RemoveMappings.class);
251         if (removeMappingsAnnotation == null) {
252             RemoveMapping removeMappingAnnotation = AnnotationUtils.findAnnotation(classDescriptor.getJavaClass(),
253                     RemoveMapping.class);
254             if (removeMappingAnnotation != null) {
255                 removeMappings.add(removeMappingAnnotation);
256             }
257         } else {
258             for (RemoveMapping removeMapping : removeMappingsAnnotation.value()) {
259                 removeMappings.add(removeMapping);
260             }
261         }
262         return removeMappings;
263     }
264 
265     /**
266      * Gets any {@link Sequence} from the session.
267      *
268      * @param session the current session.
269      * @return a list of {@link Sequence}s.
270      */
271     protected List<Sequence> loadSequences(Session session) {
272         Map<Class, ClassDescriptor> descriptors = session.getDescriptors();
273         List<PortableSequenceGenerator> sequenceGenerators = new ArrayList<PortableSequenceGenerator>();
274         for (Class<?> entityClass : descriptors.keySet()) {
275             PortableSequenceGenerator sequenceGenerator = AnnotationUtils.findAnnotation(entityClass,
276                     PortableSequenceGenerator.class);
277             if (sequenceGenerator != null) {
278                 sequenceGenerators.add(sequenceGenerator);
279             }
280             loadFieldSequences(entityClass, sequenceGenerators);
281             for (Method method : entityClass.getMethods()) {
282                 PortableSequenceGenerator methodSequenceGenerator = method.getAnnotation(
283                         PortableSequenceGenerator.class);
284                 if (methodSequenceGenerator != null) {
285                     sequenceGenerators.add(methodSequenceGenerator);
286                 }
287             }
288         }
289         List<Sequence> sequences = new ArrayList<Sequence>();
290         for (PortableSequenceGenerator sequenceGenerator : sequenceGenerators) {
291             Sequence sequence = new MaxValueIncrementerSequenceWrapper(sequenceGenerator);
292             sequences.add(sequence);
293         }
294         return sequences;
295     }
296 
297     /**
298      * Loads any field-based sequences from the given type.
299      *
300      * @param entityClass the type of the entity.
301      * @param sequenceGenerators the current list of sequence generators.
302      */
303     protected void loadFieldSequences(Class<?> entityClass, List<PortableSequenceGenerator> sequenceGenerators) {
304         for (Field field : entityClass.getDeclaredFields()) {
305             PortableSequenceGenerator fieldSequenceGenerator = field.getAnnotation(PortableSequenceGenerator.class);
306             if (fieldSequenceGenerator != null) {
307                 sequenceGenerators.add(fieldSequenceGenerator);
308             }
309         }
310         // next, walk up and check the super class...
311         if (entityClass.getSuperclass() != null) {
312             loadFieldSequences(entityClass.getSuperclass(), sequenceGenerators);
313         }
314     }
315 
316     /**
317      * Translates our {@link PortableSequenceGenerator} into an EclipseLink {@link Sequence}.
318      */
319     private static final class MaxValueIncrementerSequenceWrapper extends Sequence {
320 
321         private static final long serialVersionUID = 2375805962996574386L;
322 
323         private final String sequenceName;
324 
325         /**
326          * Creates a sequence wrapper for our {@link PortableSequenceGenerator}.
327          *
328          * @param sequenceGenerator the {@link PortableSequenceGenerator} to process.
329          */
330         MaxValueIncrementerSequenceWrapper(PortableSequenceGenerator sequenceGenerator) {
331             super(sequenceGenerator.name(), 0);
332             // default sequenceName to the name of the sequence generator if the sequence name was not provided
333             if (StringUtils.isBlank(sequenceGenerator.sequenceName())) {
334                 sequenceName = sequenceGenerator.name();
335             } else {
336                 sequenceName = sequenceGenerator.sequenceName();
337             }
338         }
339 
340         /**
341          * {@inheritDoc}
342          */
343         @Override
344         public boolean shouldAcquireValueAfterInsert() {
345             return false;
346         }
347 
348         /**
349          * {@inheritDoc}
350          */
351         @Override
352         public boolean shouldUseTransaction() {
353             return true;
354         }
355 
356         /**
357          * {@inheritDoc}
358          */
359         @Override
360         public boolean shouldUsePreallocation() {
361             return false;
362         }
363 
364         /**
365          * {@inheritDoc}
366          */
367         @Override
368         public Object getGeneratedValue(Accessor accessor, AbstractSession writeSession, String seqName) {
369             DataSource dataSource = ((JNDIConnector) writeSession.getLogin().getConnector()).getDataSource();
370             DataFieldMaxValueIncrementer incrementer = MaxValueIncrementerFactory.getIncrementer(dataSource,
371                     sequenceName);
372             return Long.valueOf(incrementer.nextLongValue());
373         }
374 
375         /**
376          * {@inheritDoc}
377          */
378         @Override
379         public Vector<?> getGeneratedVector(Accessor accessor, AbstractSession writeSession, String seqName, int size) {
380             // we're not in the business of pre-fetching/allocating ids
381             throw new UnsupportedOperationException(getClass().getName() + " does pre-generate sequence ids");
382         }
383 
384         /**
385          * {@inheritDoc}
386          */
387         @Override
388         public void onConnect() {}
389 
390         /**
391          * {@inheritDoc}
392          */
393         @Override
394         public void onDisconnect() {}
395 
396         /**
397          * {@inheritDoc}
398          */
399         @Override
400         public MaxValueIncrementerSequenceWrapper clone() {
401             return (MaxValueIncrementerSequenceWrapper) super.clone();
402         }
403 
404     }
405 
406 }