001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.data.jpa.eclipselink;
017
018import org.apache.commons.lang.StringUtils;
019import org.eclipse.persistence.config.SessionCustomizer;
020import org.eclipse.persistence.descriptors.ClassDescriptor;
021import org.eclipse.persistence.exceptions.DescriptorException;
022import org.eclipse.persistence.internal.databaseaccess.Accessor;
023import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
024import org.eclipse.persistence.internal.sessions.AbstractSession;
025import org.eclipse.persistence.mappings.DatabaseMapping;
026import org.eclipse.persistence.sequencing.Sequence;
027import org.eclipse.persistence.sessions.DatabaseLogin;
028import org.eclipse.persistence.sessions.JNDIConnector;
029import org.eclipse.persistence.sessions.Session;
030import org.kuali.rice.krad.data.jpa.DisableVersioning;
031import org.kuali.rice.krad.data.jpa.Filter;
032import org.kuali.rice.krad.data.jpa.FilterGenerator;
033import org.kuali.rice.krad.data.jpa.FilterGenerators;
034import org.kuali.rice.krad.data.jpa.PortableSequenceGenerator;
035import org.kuali.rice.krad.data.jpa.RemoveMapping;
036import org.kuali.rice.krad.data.jpa.RemoveMappings;
037import org.kuali.rice.krad.data.platform.MaxValueIncrementerFactory;
038import org.springframework.core.annotation.AnnotationUtils;
039import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
040
041import javax.sql.DataSource;
042import java.lang.reflect.Field;
043import java.lang.reflect.Method;
044import java.util.ArrayList;
045import java.util.Arrays;
046import java.util.List;
047import java.util.Map;
048import java.util.Vector;
049import java.util.concurrent.ConcurrentHashMap;
050import java.util.concurrent.ConcurrentMap;
051
052/**
053 * EclipseLink Session Customizer which understands {@link org.kuali.rice.krad.data.jpa.PortableSequenceGenerator}
054 * annotations and automatically registers custom EclipseLink Sequences.
055 *
056 * <p>
057 * Since SessionCustomizers are stateless instances, and because concrete
058 * {@link org.eclipse.persistence.sequencing.Sequence} objects must be registered individually with the EclipseLink
059 * session, we lazy generate the Sequence objects using annotation inspection and then register them on each new
060 * session using this customizer.
061 * </p>
062 *
063 * @author Kuali Rice Team (rice.collab@kuali.org)
064 */
065public class KradEclipseLinkCustomizer implements SessionCustomizer {
066
067    private static ConcurrentMap<String, List<Sequence>> sequenceMap = new ConcurrentHashMap<String, List<Sequence>>(8,
068            0.9f, 1);
069
070    /* Keyed by the session name determines if the class descriptors have been modified for the current session. */
071    private static ConcurrentMap<String, Boolean> modDescMap = new ConcurrentHashMap<String, Boolean>();
072
073    private static ConcurrentMap<String, List<FilterGenerator>> queryCustomizerMap =
074            new ConcurrentHashMap<String, List<FilterGenerator>>();
075
076    /**
077     * {@inheritDoc}
078     */
079    @Override
080    public void customize(Session session) throws Exception {
081        String sessionName = session.getName();
082
083        // double-checked locking on ConcurrentMap
084        List<Sequence> sequences = sequenceMap.get(sessionName);
085        if (sequences == null) {
086            sequences = sequenceMap.putIfAbsent(sessionName, loadSequences(session));
087            if (sequences == null) {
088                sequences = sequenceMap.get(sessionName);
089            }
090        }
091
092        loadQueryCustomizers(session);
093
094        DatabaseLogin login = session.getLogin();
095        for (Sequence sequence : sequences) {
096            login.addSequence(sequence);
097        }
098
099        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}