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}