/**
  * Performs a deep comparison (using reflection) of two objects to determine whether they are
  * different.
  *
  * <p>If the object is a Question then the answer is treated specially because the original value
  * of the answer from our point of view is the value provided by the client in an Answer fact. If
  * no such fact exists then the value on the question itself is used. Scenarios are:
  *
  * <ul>
  *   <li>Question which client has just answered - the new object is different if the answer is
  *       not the value provided by the client. i.e. if the rules have changed it to something else
  *       e.g. converting text to upper case.
  *   <li>Another question - the new object is different if the answer is not the value on the
  *       original object.
  * </ul>
  *
  * @param originalObject
  * @param newObject
  * @return
  */
 private boolean different(TohuObject originalObject, TohuObject newObject) {
   if (!originalObject.equals(newObject)) {
     return true;
   }
   // special handling for Question answers
   if (originalObject instanceof Question) {
     Question originalQuestion = (Question) originalObject;
     String originalAnswer;
     if (clientAnswers != null && clientAnswers.containsKey(originalQuestion.getId())) {
       // original answer is the one provided by the client
       originalAnswer = clientAnswers.get(originalQuestion.getId());
     } else {
       // original answer not provided by client so is contained in the original question
       originalAnswer =
           originalQuestion.getAnswer() == null ? null : originalQuestion.getAnswer().toString();
     }
     Question newQuestion = (Question) newObject;
     String newAnswer =
         newQuestion.getAnswer() == null ? null : newQuestion.getAnswer().toString();
     if (originalAnswer == null ? newAnswer != null : !originalAnswer.equals(newAnswer)) {
       return true;
     }
   }
   Class<?> clazz = originalObject.getClass();
   do {
     // compare all non-static non-transient fields
     for (Field field : clazz.getDeclaredFields()) {
       int modifiers = field.getModifiers();
       if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) {
         boolean answerField = field.isAnnotationPresent(Question.AnswerField.class);
         // answer fields are skipped because we have checked this already
         if (!answerField) {
           field.setAccessible(true);
           try {
             Object originalValue = field.get(originalObject);
             Object newValue = field.get(newObject);
             if (originalValue == null ? newValue != null : !originalValue.equals(newValue)) {
               return true;
             }
           } catch (IllegalArgumentException e) {
             throw new RuntimeException(e);
           } catch (IllegalAccessException e) {
             throw new RuntimeException(e);
           }
         }
       }
     }
     clazz = clazz.getSuperclass();
   } while (clazz != null);
   return false;
 }
 /**
  * @see
  *     org.drools.event.rule.WorkingMemoryEventListener#objectRetracted(org.drools.event.rule.ObjectRetractedEvent)
  */
 public void objectRetracted(ObjectRetractedEvent event) {
   logger.debug(
       "==> [ObjectRetracted: handle="
           + event.getFactHandle()
           + "; object="
           + event.getOldObject()
           + "]");
   if (event.getOldObject() instanceof TohuObject) {
     TohuObject oldObject = (TohuObject) event.getOldObject();
     String id = oldObject.getId();
     logger.debug("==> ObjectRemoved: Removing Fact with ID [" + id + "] from Working Memry");
     TohuObject originalObject = getOriginalObject(id);
     processChange(id, originalObject, null, oldObject, event.getFactHandle());
   }
 }
 /**
  * @see
  *     org.drools.event.rule.WorkingMemoryEventListener#objectUpdated(org.drools.event.rule.ObjectUpdatedEvent)
  */
 public void objectUpdated(ObjectUpdatedEvent event) {
   logger.debug(
       "==> [ObjectUpdated handle="
           + event.getFactHandle()
           + "; object="
           + event.getOldObject()
           + "]");
   if (event.getObject() instanceof TohuObject) {
     TohuObject newObject = (TohuObject) event.getObject();
     String id = newObject.getId();
     logger.debug(
         "==> ObjectUpdated: Updating Fact with ID [" + id + "] that exists in Working Memry");
     TohuObject originalObject = getOriginalObject(id);
     processChange(id, originalObject, newObject, newObject, event.getFactHandle());
   }
 }
 /**
  * Makes copies of the original value all the objects that we wish to track.
  *
  * <p>Shallow copies are sufficient since none of our objects contain children. (All lists are
  * stored as comma-delimited strings so they can be serialized nicely.)
  *
  * @param originalObjects
  */
 public void initialise(Collection<?> originalObjects) {
   this.originalObjects = new HashMap<String, TohuObject>();
   for (Object object : originalObjects) {
     if (object instanceof TohuObject) {
       TohuObject i = (TohuObject) object;
       try {
         this.originalObjects.put(i.getId(), (TohuObject) i.clone());
       } catch (CloneNotSupportedException e) {
         // ignore
       }
     } else if (object instanceof Answer) {
       Answer answer = (Answer) object;
       storeClientAnswer(answer);
     }
   }
 }
 /**
  * @see
  *     org.drools.event.rule.WorkingMemoryEventListener#objectInserted(org.drools.event.rule.ObjectInsertedEvent)
  */
 public void objectInserted(ObjectInsertedEvent event) {
   logger.debug(
       "==> [ObjectInserted: handle="
           + event.getFactHandle()
           + "; object="
           + event.getObject()
           + "]");
   if (event.getObject() instanceof TohuObject) {
     TohuObject newObject = (TohuObject) event.getObject();
     String id = newObject.getId();
     TohuObject originalObject = getOriginalObject(id);
     logger.debug(
         "==>ObjectInserted: Inserting Tohu Fact with ID [" + id + "] into working memry");
     processChange(id, originalObject, newObject, newObject, event.getFactHandle());
   } else if (event.getObject() instanceof Answer) {
     Answer answer = (Answer) event.getObject();
     logger.debug(
         "==>ObjectInserted: Inserting Answer Fact with value ["
             + answer.getValue()
             + "] into working memry");
     storeClientAnswer(answer);
   }
 }
 /**
  * Processes an object change from originalObject to newObject and determines whether this is a
  * create, update or delete.
  *
  * <p>Replaces any previous change for the same object.
  *
  * @param id
  * @param originalObject
  * @param newObject
  * @param recentObject A recent instance of this object which is used only for removing objects
  *     from lists. This is required because it is possible for both oldObject and newObject to be
  *     null if we are processing a delete right after a create.
  */
 private void processChange(
     String id,
     TohuObject originalObject,
     TohuObject newObject,
     TohuObject recentObject,
     FactHandle factHandle) {
   // remove previous change
   if (create != null) {
     create.remove(recentObject);
   }
   if (update != null) {
     update.remove(recentObject);
   }
   if (delete != null) {
     if (recentObject instanceof InvalidAnswer) {
       delete.remove(recentObject);
     } else {
       delete.remove(new ItemId((Item) recentObject));
     }
   }
   // determine what we need to do
   boolean isCreate =
       (originalObject == null || !originalObject.isActive())
           && newObject != null
           && newObject.isActive();
   boolean isUpdate =
       originalObject != null
           && originalObject.isActive()
           && newObject != null
           && newObject.isActive()
           && different(originalObject, newObject);
   boolean isDelete =
       originalObject != null
           && originalObject.isActive()
           && (newObject == null || !newObject.isActive());
   // make the change
   if (isCreate) {
     if (create == null) {
       create = new HashMap<Object, FactHandle>();
     }
     create.put(newObject, factHandle);
   }
   if (isUpdate) {
     if (update == null) {
       update = new ArrayList<Object>();
     }
     update.add(newObject);
   }
   if (isDelete) {
     if (delete == null) {
       delete = new ArrayList<Object>();
     }
     if (recentObject instanceof InvalidAnswer) {
       delete.add(recentObject);
     } else {
       delete.add(new ItemId((Item) recentObject));
     }
   }
   // remove empty lists
   if (create != null && create.isEmpty()) {
     create = null;
   }
   if (update != null && update.isEmpty()) {
     update = null;
   }
   if (delete != null && delete.isEmpty()) {
     delete = null;
   }
 }