Coverage Report - org.kuali.rice.kim.service.impl.IdentityArchiveServiceImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
IdentityArchiveServiceImpl
0%
0/42
0%
0/12
2.292
IdentityArchiveServiceImpl$1
N/A
N/A
2.292
IdentityArchiveServiceImpl$CallableAdapter
0%
0/8
N/A
2.292
IdentityArchiveServiceImpl$EntityArchiveWriter
0%
0/11
N/A
2.292
IdentityArchiveServiceImpl$EntityArchiveWriter$1
0%
0/8
0%
0/8
2.292
IdentityArchiveServiceImpl$EntityArchiveWriter$2
0%
0/20
0%
0/14
2.292
IdentityArchiveServiceImpl$EntityArchiveWriter$3
0%
0/11
0%
0/8
2.292
IdentityArchiveServiceImpl$PreLogCallableWrapper
0%
0/7
N/A
2.292
IdentityArchiveServiceImpl$WriteQueue
0%
0/8
0%
0/2
2.292
 
 1  
 /*
 2  
  * Copyright 2006-2011 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  
 
 17  
 package org.kuali.rice.kim.service.impl;
 18  
 
 19  
 import org.apache.commons.lang.StringUtils;
 20  
 import org.apache.log4j.Level;
 21  
 import org.apache.log4j.Logger;
 22  
 import org.kuali.rice.core.api.config.property.ConfigurationService;
 23  
 import org.kuali.rice.kim.api.identity.entity.EntityDefault;
 24  
 import org.kuali.rice.kim.api.identity.principal.Principal;
 25  
 import org.kuali.rice.kim.api.services.IdentityArchiveService;
 26  
 import org.kuali.rice.kim.bo.entity.impl.KimEntityDefaultInfoCacheImpl;
 27  
 import org.kuali.rice.kim.util.KimConstants;
 28  
 import org.kuali.rice.krad.service.BusinessObjectService;
 29  
 import org.kuali.rice.krad.service.KRADServiceLocatorInternal;
 30  
 import org.kuali.rice.ksb.service.KSBServiceLocator;
 31  
 import org.springframework.beans.factory.DisposableBean;
 32  
 import org.springframework.beans.factory.InitializingBean;
 33  
 import org.springframework.transaction.PlatformTransactionManager;
 34  
 import org.springframework.transaction.TransactionStatus;
 35  
 import org.springframework.transaction.support.TransactionCallback;
 36  
 import org.springframework.transaction.support.TransactionTemplate;
 37  
 
 38  
 import java.util.ArrayList;
 39  
 import java.util.Arrays;
 40  
 import java.util.Collection;
 41  
 import java.util.Collections;
 42  
 import java.util.Comparator;
 43  
 import java.util.HashMap;
 44  
 import java.util.HashSet;
 45  
 import java.util.List;
 46  
 import java.util.Map;
 47  
 import java.util.Set;
 48  
 import java.util.concurrent.Callable;
 49  
 import java.util.concurrent.ConcurrentLinkedQueue;
 50  
 import java.util.concurrent.TimeUnit;
 51  
 import java.util.concurrent.atomic.AtomicBoolean;
 52  
 import java.util.concurrent.atomic.AtomicInteger;
 53  
 
 54  
 /**
 55  
  * This is the default implementation for the IdentityArchiveService.
 56  
  * @see IdentityArchiveService
 57  
  * @author Kuali Rice Team (rice.collab@kuali.org)
 58  
  *
 59  
  */
 60  0
 public class IdentityArchiveServiceImpl implements IdentityArchiveService, InitializingBean, DisposableBean {
 61  0
         private static final Logger LOG = Logger.getLogger( IdentityArchiveServiceImpl.class );
 62  
 
 63  
         private BusinessObjectService businessObjectService;
 64  
         private ConfigurationService kualiConfigurationService;
 65  
 
 66  
         private static final String EXEC_INTERVAL_SECS = "kim.identityArchiveServiceImpl.executionIntervalSeconds";
 67  
         private static final String MAX_WRITE_QUEUE_SIZE = "kim.identityArchiveServiceImpl.maxWriteQueueSize";
 68  
         private static final int EXECUTION_INTERVAL_SECONDS_DEFAULT = 600; // by default, flush the write queue this often
 69  
         private static final int MAX_WRITE_QUEUE_SIZE_DEFAULT = 300; // cache this many KEDI's before forcing write
 70  
 
 71  0
         private final WriteQueue writeQueue = new WriteQueue();
 72  0
         private final EntityArchiveWriter writer = new EntityArchiveWriter();
 73  
 
 74  
         // all this ceremony just decorates the writer so it logs a message first, and converts the Callable to Runnable
 75  0
         private final Runnable maxQueueSizeExceededWriter =
 76  
                 new CallableAdapter(new PreLogCallableWrapper<Boolean>(writer, Level.DEBUG, "max size exceeded, flushing write queue"));
 77  
 
 78  
         // ditto
 79  0
         private final Runnable scheduledWriter =
 80  
                 new CallableAdapter(new PreLogCallableWrapper<Boolean>(writer, Level.DEBUG, "scheduled write out, flushing write queue"));
 81  
 
 82  
         // ditto
 83  0
         private final Runnable shutdownWriter =
 84  
                 new CallableAdapter(new PreLogCallableWrapper<Boolean>(writer, Level.DEBUG, "rice is shutting down, flushing write queue"));
 85  
         
 86  
         private int getExecutionIntervalSeconds() {
 87  0
                 final String prop = kualiConfigurationService.getPropertyString(EXEC_INTERVAL_SECS);
 88  
                 try {
 89  0
                         return Integer.valueOf(prop).intValue();
 90  0
                 } catch (NumberFormatException e) {
 91  0
                         return EXECUTION_INTERVAL_SECONDS_DEFAULT;
 92  
                 }
 93  
         }
 94  
         
 95  
         private int getMaxWriteQueueSize() {
 96  0
                 final String prop = kualiConfigurationService.getPropertyString(MAX_WRITE_QUEUE_SIZE);
 97  
                 try {
 98  0
                         return Integer.valueOf(prop).intValue();
 99  0
                 } catch (NumberFormatException e) {
 100  0
                         return MAX_WRITE_QUEUE_SIZE_DEFAULT;
 101  
                 }
 102  
         }
 103  
 
 104  
         @Override
 105  
         public EntityDefault getEntityDefaultInfoFromArchive( String entityId ) {
 106  0
             Map<String,String> criteria = new HashMap<String, String>(1);
 107  0
             criteria.put(KimConstants.PrimaryKeyConstants.ENTITY_ID, entityId);
 108  0
             KimEntityDefaultInfoCacheImpl cachedValue = getBusinessObjectService().findByPrimaryKey(KimEntityDefaultInfoCacheImpl.class, criteria);
 109  0
             return (cachedValue == null) ? null : cachedValue.convertCacheToEntityDefaultInfo();
 110  
     }
 111  
 
 112  
     @Override
 113  
         public EntityDefault getEntityDefaultInfoFromArchiveByPrincipalId( String principalId ) {
 114  0
             Map<String,String> criteria = new HashMap<String, String>(1);
 115  0
             criteria.put("principalId", principalId);
 116  0
             KimEntityDefaultInfoCacheImpl cachedValue = getBusinessObjectService().findByPrimaryKey(KimEntityDefaultInfoCacheImpl.class, criteria);
 117  0
             return (cachedValue == null) ? null : cachedValue.convertCacheToEntityDefaultInfo();
 118  
     }
 119  
 
 120  
     @Override
 121  
         public EntityDefault getEntityDefaultInfoFromArchiveByPrincipalName( String principalName ) {
 122  0
             Map<String,String> criteria = new HashMap<String, String>(1);
 123  0
             criteria.put("principalName", principalName);
 124  0
             Collection<KimEntityDefaultInfoCacheImpl> entities = getBusinessObjectService().findMatching(KimEntityDefaultInfoCacheImpl.class, criteria);
 125  0
             return (entities == null || entities.size() == 0) ? null : entities.iterator().next().convertCacheToEntityDefaultInfo();
 126  
     }
 127  
 
 128  
     @Override
 129  
         public void saveDefaultInfoToArchive( EntityDefault entity ) {
 130  
             // if the max size has been reached, schedule now
 131  0
             if (getMaxWriteQueueSize() <= writeQueue.offerAndGetSize(entity) /* <- this enqueues the KEDI */ &&
 132  
                             writer.requestSubmit()) {
 133  0
                     KSBServiceLocator.getThreadPool().execute(maxQueueSizeExceededWriter);
 134  
             }
 135  0
     }
 136  
     
 137  
         public BusinessObjectService getBusinessObjectService() {
 138  0
                 return this.businessObjectService;
 139  
         }
 140  
 
 141  
         public void setBusinessObjectService(BusinessObjectService businessObjectService) {
 142  0
                 this.businessObjectService = businessObjectService;
 143  0
         }
 144  
 
 145  
         public ConfigurationService getKualiConfigurationService() {
 146  0
                 return this.kualiConfigurationService;
 147  
         }
 148  
 
 149  
         public void setKualiConfigurationService(
 150  
                         ConfigurationService kualiConfigurationService) {
 151  0
                 this.kualiConfigurationService = kualiConfigurationService;
 152  0
         }
 153  
     
 154  
     /** schedule the writer on the KSB scheduled pool. */
 155  
         @Override
 156  
         public void afterPropertiesSet() throws Exception {
 157  0
                 LOG.info("scheduling writer...");
 158  0
                 KSBServiceLocator.getScheduledPool().scheduleAtFixedRate(scheduledWriter,
 159  
                                 getExecutionIntervalSeconds(), getExecutionIntervalSeconds(), TimeUnit.SECONDS);
 160  0
         }
 161  
 
 162  
         /** flush the write queue immediately. */
 163  
         @Override
 164  
         public void destroy() throws Exception {
 165  0
                 KSBServiceLocator.getThreadPool().execute(shutdownWriter);
 166  0
         }
 167  
 
 168  
         /**
 169  
          * store the person to the database, but do this an alternate thread to
 170  
          * prevent transaction issues since this service is non-transactional
 171  
          *
 172  
          * @author Kuali Rice Team (rice.collab@kuali.org)
 173  
          *
 174  
          */
 175  0
         private class EntityArchiveWriter implements Callable {
 176  
 
 177  
                 // flag used to prevent multiple processes from being submitted at once
 178  0
                 AtomicBoolean currentlySubmitted = new AtomicBoolean(false);
 179  
 
 180  0
                 private final Comparator<Comparable> nullSafeComparator = new Comparator<Comparable>() {
 181  
                         @Override
 182  
                         public int compare(Comparable i1, Comparable i2) {
 183  0
                                 if (i1 != null && i2 != null) {
 184  0
                                         return i1.compareTo(i2);
 185  0
                                 } else if (i1 == null) {
 186  0
                                         if (i2 == null) {
 187  0
                                                 return 0;
 188  
                                         } else {
 189  0
                                                 return -1;
 190  
                                         }
 191  
                                 } else { // if (entityId2 == null) {
 192  0
                                         return 1;
 193  
                                 }
 194  
                         };
 195  
                 };
 196  
 
 197  
                 /**
 198  
                  * Comparator that attempts to impose a total ordering on EntityDefault instances
 199  
                  */
 200  0
                 private final Comparator<EntityDefault> kediComparator = new Comparator<EntityDefault>() {
 201  
                         /**
 202  
                          * compares by entityId value
 203  
                          * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
 204  
                          */
 205  
                         @Override
 206  
                         public int compare(EntityDefault o1, EntityDefault o2) {
 207  0
                                 String entityId1 = (o1 == null) ? null : o1.getEntityId();
 208  0
                                 String entityId2 = (o2 == null) ? null : o2.getEntityId();
 209  
 
 210  0
                                 int result = nullSafeComparator.compare(entityId1, entityId2);
 211  
 
 212  0
                                 if (result == 0) {
 213  0
                                         result = getPrincipalIdsString(o1).compareTo(getPrincipalIdsString(o2));
 214  
                                 }
 215  
 
 216  0
                                 return result;
 217  
                         }
 218  
 
 219  
                         /**
 220  
                          * This method builds a newline delimited String containing the identity's principal IDs in sorted order
 221  
                          *
 222  
                          * @param entity
 223  
                          * @return
 224  
                          */
 225  
                         private String getPrincipalIdsString(EntityDefault entity) {
 226  0
                                 String result = "";
 227  0
                                 if (entity != null) {
 228  0
                                         List<Principal> principals = entity.getPrincipals();
 229  0
                                         if (principals != null) {
 230  0
                                                 if (principals.size() == 1) { // one
 231  0
                                                         result = principals.get(0).getPrincipalId();
 232  
                                                 } else { // multiple
 233  0
                                                         String [] ids = new String [principals.size()];
 234  0
                                                         int insertIndex = 0;
 235  0
                                                         for (Principal principal : principals) {
 236  0
                                                                 ids[insertIndex++] = principal.getPrincipalId();
 237  
                                                         }
 238  0
                                                         Arrays.sort(ids);
 239  0
                                                         result = StringUtils.join(ids, "\n");
 240  
                                                 }
 241  
                                         }
 242  
                                 }
 243  0
                                 return result;
 244  
                         }
 245  
                 };
 246  
 
 247  
                 public boolean requestSubmit() {
 248  0
                         return currentlySubmitted.compareAndSet(false, true);
 249  
                 }
 250  
 
 251  
                 /**
 252  
                  * Call that tries to flush the write queue.
 253  
                  * @see Callable#call()
 254  
                  */
 255  
                 @Override
 256  
                 public Object call() {
 257  
                         try {
 258  
                                 // the strategy is to grab chunks of entities, dedupe & sort them, and insert them in a big
 259  
                                 // batch to reduce transaction overhead.  Sorting is done so insertion order is guaranteed, which
 260  
                                 // prevents deadlocks between concurrent writers to the database.
 261  0
                                 PlatformTransactionManager transactionManager = KRADServiceLocatorInternal.getTransactionManager();
 262  0
                                 TransactionTemplate template = new TransactionTemplate(transactionManager);
 263  0
                                 template.execute(new TransactionCallback() {
 264  
                                         @Override
 265  
                                         public Object doInTransaction(TransactionStatus status) {
 266  0
                                                 EntityDefault entity = null;
 267  0
                                                 ArrayList<EntityDefault> entitiesToInsert = new ArrayList<EntityDefault>(getMaxWriteQueueSize());
 268  0
                                                 Set<String> deduper = new HashSet<String>(getMaxWriteQueueSize());
 269  
 
 270  
                                                 // order is important in this conditional so that elements aren't dequeued and then ignored
 271  0
                                                 while (entitiesToInsert.size() < getMaxWriteQueueSize() && null != (entity = writeQueue.poll())) {
 272  0
                                                         if (deduper.add(entity.getEntityId())) {
 273  0
                                                                 entitiesToInsert.add(entity);
 274  
                                                         }
 275  
                                                 }
 276  
 
 277  0
                                                 Collections.sort(entitiesToInsert, kediComparator);
 278  
 
 279  0
                                                 for (EntityDefault entityToInsert : entitiesToInsert) {
 280  0
                                                         getBusinessObjectService().save( new KimEntityDefaultInfoCacheImpl( entityToInsert ) );
 281  
                                                 }
 282  0
                                                 return null;
 283  
                                         }
 284  
                                 });
 285  
                         } finally { // make sure our running flag is unset, otherwise we'll never run again
 286  0
                                 currentlySubmitted.compareAndSet(true, false);
 287  0
                         }
 288  
 
 289  0
                         return Boolean.TRUE;
 290  
                 }
 291  
         }
 292  
 
 293  
         /**
 294  
          * A class encapsulating a {@link ConcurrentLinkedQueue} and an {@link AtomicInteger} to
 295  
          * provide fast offer(enqueue)/poll(dequeue) and size checking.  Size may be approximate due to concurrent
 296  
          * activity, but for our purposes that is fine.
 297  
          *
 298  
          * @author Kuali Rice Team (rice.collab@kuali.org)
 299  
          *
 300  
          */
 301  0
         private static class WriteQueue {
 302  0
                 AtomicInteger writeQueueSize = new AtomicInteger(0);
 303  0
                 ConcurrentLinkedQueue<EntityDefault> queue = new ConcurrentLinkedQueue<EntityDefault>();
 304  
 
 305  
                 public int offerAndGetSize(EntityDefault entity) {
 306  0
                         queue.add(entity);
 307  0
                         return writeQueueSize.incrementAndGet();
 308  
                 }
 309  
 
 310  
                 private EntityDefault poll() {
 311  0
                         EntityDefault result = queue.poll();
 312  0
                         if (result != null) { writeQueueSize.decrementAndGet(); }
 313  0
                         return result;
 314  
                 }
 315  
         }
 316  
 
 317  
         /**
 318  
          * decorator for a callable to log a message before it is executed
 319  
          *
 320  
          * @author Kuali Rice Team (rice.collab@kuali.org)
 321  
          *
 322  
          */
 323  
         private static class PreLogCallableWrapper<A> implements Callable<A> {
 324  
                 
 325  
                 private final Callable inner;
 326  
                 private final Level level;
 327  
                 private final String message;
 328  
 
 329  0
                 public PreLogCallableWrapper(Callable inner, Level level, String message) {
 330  0
                         this.inner = inner;
 331  0
                         this.level = level;
 332  0
                         this.message = message;
 333  0
                 }
 334  
 
 335  
                 /**
 336  
                  * logs the message then calls the inner Callable
 337  
                  * 
 338  
                  * @see java.util.concurrent.Callable#call()
 339  
                  */
 340  
                 @Override
 341  
                 @SuppressWarnings("unchecked")
 342  
                 public A call() throws Exception {
 343  0
                         LOG.log(level, message);
 344  0
                         return (A)inner.call();
 345  
                 }
 346  
         }
 347  
 
 348  
         /**
 349  
          * Adapts a Callable to be Runnable
 350  
          *
 351  
          * @author Kuali Rice Team (rice.collab@kuali.org)
 352  
          *
 353  
          */
 354  0
         private static class CallableAdapter implements Runnable {
 355  
 
 356  
                 private final Callable callable;
 357  
 
 358  0
                 public CallableAdapter(Callable callable) {
 359  0
                         this.callable = callable;
 360  0
                 }
 361  
 
 362  
                 @Override
 363  
                 public void run() {
 364  
                         try {
 365  0
                                 callable.call();
 366  0
                         } catch (Exception e) {
 367  0
                                 throw new RuntimeException(e);
 368  0
                         }
 369  0
                 }
 370  
         }
 371  
 }