/*
* Copyright 2011 JBoss Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.drools.informer.xml;
import org.drools.event.rule.ObjectInsertedEvent;
import org.drools.event.rule.ObjectRetractedEvent;
import org.drools.event.rule.ObjectUpdatedEvent;
import org.drools.event.rule.WorkingMemoryEventListener;
import org.drools.informer.*;
import org.drools.runtime.rule.FactHandle;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* <p>
* Builds up lists of the Tohu objects created, updated or deleted as a result of a question being answered.
* </p>
* <p>
* These lists are built up as the changes occur because they are read via reflection and we won't know when this happens.
* </p>
* <p>
* There is special handling for:
* </p>
* <ul>
* <li>Inactive objects - From the client's perspective these do not exist. Therefore:
* <ol>
* <li> an object becoming inactive is effectively a delete</li>
* <li> an object becoming active is effectively a create</li>
* <li> changes to inactive objects are ignored</li>
* </ol>
* </li>
* <li>Question answers - if the client has just answered a question then we care about changes from that value not from the
* value that was previously on the Question object.</li>
* </ul>
*
* @author Damon Horrell
*/
public class ChangeCollector implements WorkingMemoryEventListener
{
// private final static Logger logger = LoggerFactory.getLogger(ChangeCollector.class);
/**
* The original values of the objects we have seen. This map will contain nulls to indicate that the "original" of an object
* was that it didn't exist. i.e. prior to a create.
*/
private transient Map<String, InformerObject> originalObjects;
/**
* Answers provided by the client. This is used to avoid sending back a question to the client if the only change made to the
* question was the answer that the client provided.
*/
private transient Map<String, String> clientAnswers;
private Map<Object, FactHandle> create;
private List<Object> update;
// delete list contains ItemId and InvalidAnswer
private List<Object> delete;
public void clear() {
if (originalObjects != null)
originalObjects.clear();
if (clientAnswers != null)
clientAnswers.clear();
if (create != null)
create.clear();
if (update != null)
update.clear();
if (delete != null)
delete.clear();
}
public Map<Object, FactHandle> getCreate() {
return create;
}
public List<Object> getCreateList() {
return create == null ? Collections.<Object>emptyList() : new ArrayList(create.keySet());
}
public List<Object> getUpdate() {
return update;
}
public List<Object> getDelete() {
return delete;
}
public boolean initialised() {
return originalObjects != null;
}
/**
* <p>
* Makes copies of the original value all the objects that we wish to track.
* </p>
* <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.)
* </p>
*
* @param originalObjects
*/
public void initialise(Collection<?> originalObjects) {
this.originalObjects = new HashMap<String, InformerObject>();
for (Object object : originalObjects) {
if (object instanceof InformerObject) {
InformerObject i = (InformerObject) object;
try {
this.originalObjects.put(i.getId(), (InformerObject) 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 InformerObject) {
InformerObject newObject = (InformerObject) event.getObject();
String id = newObject.getId();
InformerObject 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);
}
}
/**
* @see org.drools.event.rule.WorkingMemoryEventListener#objectUpdated(org.drools.event.rule.ObjectUpdatedEvent)
*/
public void objectUpdated(ObjectUpdatedEvent event) {
// System.out.println("==> [ObjectUpdated handle=" + event.getFactHandle() + "; object=" + event.getOldObject() + "]");
if (event.getObject() instanceof InformerObject) {
InformerObject newObject = (InformerObject) event.getObject();
String id = newObject.getId();
// logger.debug("==> ObjectUpdated: Updating Fact with ID [" + id + "] that exists in Working Memry");
InformerObject originalObject = getOriginalObject(id);
processChange(id, originalObject, newObject, newObject, event.getFactHandle());
}
}
/**
* @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 InformerObject) {
InformerObject oldObject = (InformerObject) event.getOldObject();
String id = oldObject.getId();
// logger.debug("==> ObjectRemoved: Removing Fact with ID [" + id + "] from Working Memry");
InformerObject originalObject = getOriginalObject(id);
processChange(id, originalObject, null, oldObject, event.getFactHandle());
}
}
/**
* Fetches the original version of the object for the specified id. If we don't have one then this must be a new object being
* created so we add it (as null to indicate that it didn't orginally exist).
*
* @param id
* @return
*/
private InformerObject getOriginalObject(String id) {
if (originalObjects == null) {
originalObjects = new HashMap<String, InformerObject>();
}
boolean exists = originalObjects.containsKey(id);
if (exists) {
return originalObjects.get(id);
}
originalObjects.put(id, null);
return null;
}
/**
* <p>
* Processes an object change from originalObject to newObject and determines whether this is a create, update or delete.
* </p>
* <p>
* Replaces any previous change for the same object.
* </p>
*
* @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, InformerObject originalObject, InformerObject newObject,
InformerObject 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 LinkedHashMap<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;
}
}
/**
* <p>
* Performs a deep comparison (using reflection) of two objects to determine whether they are different.
* </p>
* <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:
* </p>
*
* <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>
* <li>Another question - the new object is different if the answer is not the value on the original object.</li>
* </ul>
*
*
* @param originalObject
* @param newObject
* @return
*/
private boolean different(InformerObject originalObject, InformerObject 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;
}
private void storeClientAnswer(Answer answer) {
if (clientAnswers == null) {
clientAnswers = new HashMap<String, String>();
}
String answerValue = answer.getValue();
// TODO should we really be handling "null" - see TOHU-3
if (answerValue != null && (answerValue.equals("") || answerValue.equals("null"))) {
answerValue = null;
}
clientAnswers.put(answer.getQuestionId(), answerValue);
}
@Override
public String toString() {
return "ChangeCollector{" +
// "originalObjects=" + originalObjects +
", clientAnswers=" + clientAnswers +
", create=" + create +
", update=" + update +
", delete=" + delete +
'}';
}
}