View Javadoc

1   /*
2    * ============================================================================
3    *                   GNU Lesser General Public License
4    * ============================================================================
5    *
6    * Taylor - The Java Enterprise Application Framework.
7    * Copyright (C) 2005 John Gilbert jgilbert01@users.sourceforge.net
8    *
9    * This library is free software; you can redistribute it and/or
10   * modify it under the terms of the GNU Lesser General Public
11   * License as published by the Free Software Foundation; either
12   * version 2.1 of the License, or (at your option) any later version.
13   *
14   * This library is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this library; if not, write to the Free Software
21   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
22   *
23   * John Gilbert
24   * Email: jgilbert01@users.sourceforge.net
25   */
26  package net.taylor.audit;
27  
28  import java.io.Serializable;
29  import java.lang.reflect.Method;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Date;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  
38  import javax.jms.JMSException;
39  import javax.jms.Message;
40  import javax.jms.ObjectMessage;
41  import javax.jms.Session;
42  import javax.persistence.Embeddable;
43  import javax.persistence.Entity;
44  
45  import net.taylor.audit.entity.Action;
46  import net.taylor.audit.entity.AuditEntry;
47  import net.taylor.audit.entity.AuditField;
48  import net.taylor.jms.JmsSender;
49  import net.taylor.jms.MessageCallback;
50  import net.taylor.jpa.JpaUtil;
51  import net.taylor.lang.ReflectionUtil;
52  import net.taylor.security.JAASUtil;
53  
54  import org.apache.commons.collections.CollectionUtils;
55  import org.apache.commons.lang.StringUtils;
56  import org.apache.commons.lang.builder.ToStringBuilder;
57  import org.apache.commons.lang.builder.ToStringStyle;
58  import org.apache.commons.logging.Log;
59  import org.apache.commons.logging.LogFactory;
60  import org.hibernate.collection.PersistentCollection;
61  import org.hibernate.engine.CollectionEntry;
62  import org.hibernate.event.AbstractCollectionEvent;
63  import org.hibernate.event.PostCollectionRecreateEvent;
64  import org.hibernate.event.PostCollectionRecreateEventListener;
65  import org.hibernate.event.PostDeleteEvent;
66  import org.hibernate.event.PostDeleteEventListener;
67  import org.hibernate.event.PostInsertEvent;
68  import org.hibernate.event.PostInsertEventListener;
69  import org.hibernate.event.PostUpdateEvent;
70  import org.hibernate.event.PostUpdateEventListener;
71  import org.hibernate.event.PreCollectionRemoveEvent;
72  import org.hibernate.event.PreCollectionRemoveEventListener;
73  import org.hibernate.event.PreCollectionUpdateEvent;
74  import org.hibernate.event.PreCollectionUpdateEventListener;
75  
76  public class AuditListener implements PostDeleteEventListener,
77  		PostInsertEventListener, PostUpdateEventListener, PreCollectionUpdateEventListener, PreCollectionRemoveEventListener,
78          PostCollectionRecreateEventListener {
79  	protected static final long serialVersionUID = 1L;
80  
81  	protected static final Log logger = LogFactory.getLog(AuditListener.class);
82  
83  	public void onPostInsert(PostInsertEvent event) {
84  		if (event.getId() == null) {
85  			return;
86  		}
87  		if (bypass(event.getEntity().getClass())) {
88  			return;
89  		}
90  
91  		queue(Action.INSERT, event.getEntity().getClass().getName(), (Long) event
92  				.getId(), event.getPersister().getPropertyNames(), null, event
93  				.getState());
94  	}
95  
96  	public void onPostUpdate(PostUpdateEvent event) {
97  		if (bypass(event.getEntity().getClass())) {
98  			return;
99  		}
100 
101 		queue(Action.UPDATE, event.getEntity().getClass().getName(), (Long) event
102 				.getId(), event.getPersister().getPropertyNames(), event
103 				.getOldState(), event.getState());
104 	}
105 
106 	public void onPostDelete(PostDeleteEvent event) {
107 		if (bypass(event.getEntity().getClass())) {
108 			return;
109 		}
110 
111 		queue(Action.DELETE, event.getEntity().getClass().getName(), (Long) event
112 				.getId(), null, null, null);
113 	}
114 
115 	@SuppressWarnings("unchecked")
116 	protected boolean bypass(Class entityClass) {
117 		if (entityClass.isAnnotationPresent(BypassAudit.class)) {
118 			return true;
119 		}
120 		return false;
121 	}
122 
123 	protected void queue(final Action action, final String entityName,
124 			final Long id, final String[] names, final Object[] oldValues,
125 			final Object[] newValues) {
126 
127 		final AuditEntry ae = createAuditEntry(entityName, id, action);
128 		if (names != null) {
129 			addAuditFields(ae, names, oldValues, newValues);
130 		}
131 
132 		publish(ae);
133 	}
134 
135 	public static final String ACTION = "action";
136 	public static final String ENTITY_NAME = "entityName";
137 	public static final String ENTITY_ID = "entityId";
138 	public static final String INSERT = "INSERT";
139 	public static final String UPDATE = "UPDATE";
140 	public static final String DELETE = "DELETE";
141 	
142 	protected void publish(final AuditEntry ae) {
143 		JmsSender.unmanagedSend("topic/net.taylor.AuditTopic", new MessageCallback() {
144 			public Message createMessage(Session session) throws JMSException {
145 				ObjectMessage msg = session.createObjectMessage();
146 				msg.setStringProperty(ACTION, ae.getAction().name());
147 				msg.setStringProperty(ENTITY_NAME, ae.getEntityName());
148 				msg.setLongProperty(ENTITY_ID, ae.getEntityId());
149 				msg.setObject(ae);
150 				return msg;
151 			}
152 		});			
153 	}
154 	
155 	protected AuditEntry createAuditEntry(String entityName, Long entityId,
156 			Action action) {
157 		AuditEntry ae = new AuditEntry();
158 		if (JAASUtil.getPrincipal() == null) {
159 			ae.setUser("system");
160 		} else {
161 			ae.setUser(JAASUtil.getPrincipal().getName());
162 		}
163 		ae.setTimestamp(new Date());
164 		ae.setEntityName(entityName);
165 		ae.setEntityId(entityId);
166 		ae.setAction(action);
167 
168 		if (logger.isDebugEnabled()) {
169 			logger.debug(ToStringBuilder.reflectionToString(ae,
170 					ToStringStyle.MULTI_LINE_STYLE));
171 		}
172 
173 		return ae;
174 	}
175 
176 	protected AuditField createAuditField(String name, Object oldValue,
177 			Object newValue) {
178 		AuditField af = new AuditField();
179 		af.setName(name);
180 		af.setNewValue(getValue(newValue));
181 		af.setOldValue(getValue(oldValue));
182 
183 		if (logger.isDebugEnabled()) {
184 			logger.debug(ToStringBuilder.reflectionToString(af,
185 					ToStringStyle.MULTI_LINE_STYLE));
186 		}
187 
188 		return af;
189 	}
190 
191 	protected String getValue(Object v) {
192 		if (v == null) {
193 			return null;
194 		}
195 		if (v.getClass().isAnnotationPresent(Entity.class)) {
196 			// handle display of m2o associations
197 			String[] names = { "getName", "getCode", "getTitle", "getDescription" };
198 			for (String name : names) {
199 				String value = get(v, name);
200 				if (value != null) {
201 					return value;
202 				}
203 			}
204 			return JpaUtil.getNaturalId(v).toString();
205 		}
206 		return v.toString();
207 	}
208 
209 	private String get(Object o, String name) {
210 		try {
211 			Method m = o.getClass().getMethod(name, new Class[] {});
212 			if (m == null) {
213 				return null;
214 			}
215 			return (String) m.invoke(o, new Object[0]);
216 		} catch (Exception ex) {
217 			return null;
218 		}
219 	}
220 
221 	protected void addAuditFields(AuditEntry ae, String[] names,
222 			Object[] oldValues, Object[] newValues) {
223 		for (int i = 0; i < names.length; i++) {
224 			Object ov = oldValues == null ? null : oldValues[i];
225 			Object nv = newValues == null ? null : newValues[i];
226 			if (isCollection(ov, nv)) {
227 				continue;
228 			} else if (isEmbedded(ov, nv)) {
229 				handleEmbedded(ae, names[i], ov, nv);
230 			} else if (hasChanged(ov, nv)) {
231 				ae.addField(createAuditField(names[i], ov, nv));
232 			}
233 		}
234 	}
235 
236 	protected boolean isCollection(Object oldValue, Object newValue) {
237 		Object value = oldValue;
238 		if (value == null) {
239 			value = newValue;
240 		}
241 		if (value == null) {
242 			return false;
243 		}
244 		return value instanceof Collection;
245 	}
246 
247 	protected boolean isEmbedded(Object oldValue, Object newValue) {
248 		Object value = oldValue;
249 		if (value == null) {
250 			value = newValue;
251 		}
252 		if (value == null) {
253 			return false;
254 		}
255 		return value.getClass().isAnnotationPresent(Embeddable.class);
256 	}
257 
258 	protected void handleEmbedded(AuditEntry ae, String name, Object oldValue,
259 			Object newValue) {
260 		try {
261 			Map<String, Object> oldValues = describe(oldValue);
262 			Map<String, Object> newValues = describe(newValue);
263 			Set<String> names = newValues.keySet();
264 			for (String key : names) {
265 				if ("class".equals(key)) {
266 					continue;
267 				}
268 				Object ov = oldValues.get(key);
269 				Object nv = newValues.get(key);
270 
271 				if (isEmbedded(ov, nv)) {
272 					handleEmbedded(ae, name + "." + key, ov, nv);
273 				} else if (hasChanged(ov, nv)) {
274 					ae.addField(createAuditField(name + "." + key, ov, nv));
275 				}
276 			}
277 		} catch (Exception e) {
278 			throw new RuntimeException(e);
279 		}
280 	}
281 
282 	protected boolean hasChanged(Object oldValue, Object newValue) {
283 		return (oldValue != null && newValue != null && !oldValue
284 				.equals(newValue))
285 				|| (oldValue != null && newValue == null)
286 				|| (oldValue == null && newValue != null);
287 	}
288 
289 	protected Map<String, Object> describe(Object object) {
290 		Map<String, Object> properties = new HashMap<String, Object>();
291 		if (object != null) {
292 			List<Method> list = new ArrayList<Method>();
293 			Method[] methods = object.getClass().getMethods();
294 			for (int i = 0; i < methods.length; i++) {
295 				Method method = methods[i];
296 				if (method.getName().startsWith("get")) {
297 					list.add(method);
298 				}
299 			}
300 			for (Method method : list) {
301 				String name = method.getName();
302 				name = name.substring(3);
303 				name = StringUtils.uncapitalize(name);
304 				properties.put(name, ReflectionUtil.invoke(method, object,
305 						new Object[] {}));
306 			}
307 		}
308 		return properties;
309 	}
310 	
311 	//---------------------------------------------------------------
312 	// collections
313 	//---------------------------------------------------------------
314 
315     public void onPreUpdateCollection(PreCollectionUpdateEvent event) {
316 		if (bypass(event.getAffectedOwnerOrNull().getClass())) {
317 			return;
318 		}
319         CollectionEntry collectionEntry = getCollectionEntry(event);	
320 //        if (!collectionEntry.getLoadedPersister().isInverse()) {
321             onCollectionAction(event, event.getCollection(), collectionEntry.getSnapshot(), collectionEntry);
322 //        }
323     }
324 
325     public void onPreRemoveCollection(PreCollectionRemoveEvent event) {
326 		if (bypass(event.getAffectedOwnerOrNull().getClass())) {
327 			return;
328 		}
329         CollectionEntry collectionEntry = getCollectionEntry(event);
330 //        if (!collectionEntry.getLoadedPersister().isInverse()) {
331             onCollectionAction(event, null, collectionEntry.getSnapshot(), collectionEntry);
332 //        }
333     }
334 
335     public void onPostRecreateCollection(PostCollectionRecreateEvent event) {
336 		if (bypass(event.getAffectedOwnerOrNull().getClass())) {
337 			return;
338 		}
339         CollectionEntry collectionEntry = getCollectionEntry(event);
340 //        if (!collectionEntry.getLoadedPersister().isInverse()) {
341             onCollectionAction(event, event.getCollection(), null, collectionEntry);
342 //        }
343     }
344 
345     protected CollectionEntry getCollectionEntry(AbstractCollectionEvent event) {
346         return event.getSession().getPersistenceContext().getCollectionEntry(event.getCollection());
347     }
348     
349     protected void onCollectionAction(AbstractCollectionEvent event, PersistentCollection newColl, Serializable oldColl,
350             CollectionEntry collectionEntry) {
351 
352 		final AuditEntry ae = createAuditEntry(event
353 				.getAffectedOwnerEntityName(), (Long) event
354 				.getAffectedOwnerIdOrNull(), Action.UPDATE);
355 		
356 		Collection<?> added = (newColl instanceof Collection ? (Collection<?>) newColl : null);
357 		Collection<?> removed = (oldColl instanceof Collection ? (Collection<?>) oldColl : null);
358 		if (added != null && removed != null) {
359 			Collection<?> intersection = CollectionUtils.intersection(added, removed);
360 			added.removeAll(intersection);
361 			removed.removeAll(intersection);
362 		}
363 
364 		if (added != null) {
365 			String role = event.getCollection().getRole();
366 			if (role != null) {
367 				role = role.substring(role.lastIndexOf(".") + 1);
368 			}
369 			for (Object object : added) {
370 				AuditField af = new AuditField();
371 				af.setName(role);
372 				String value = JpaUtil.getNaturalId(object).toString();
373 				value = value.replace("{", "").replace("}", "");
374 				af.setNewValue(getValue(value));
375 				ae.addField(af);
376 			}
377 		}
378 
379 		if (removed != null) {
380 			String role = event.getCollection().getRole();
381 			if (role != null) {
382 				role = role.substring(role.lastIndexOf(".") + 1);
383 			}
384 			for (Object object : removed) {
385 				AuditField af = new AuditField();
386 				af.setName(role);
387 				String value = JpaUtil.getNaturalId(object).toString();
388 				value = value.replace("{", "").replace("}", "");
389 				af.setOldValue(getValue(value));
390 				ae.addField(af);
391 			}
392 		}
393 
394 		if (!ae.getFields().isEmpty()) {
395 			publish(ae);
396 		}
397 	}	
398 }