View Javadoc
1   package org.argeo.connect.versioning;
2   
3   import static java.util.Arrays.asList;
4   
5   import java.util.ArrayList;
6   import java.util.Calendar;
7   import java.util.Iterator;
8   import java.util.LinkedHashMap;
9   import java.util.List;
10  import java.util.Map;
11  
12  import javax.jcr.Item;
13  import javax.jcr.Node;
14  import javax.jcr.NodeIterator;
15  import javax.jcr.Property;
16  import javax.jcr.PropertyIterator;
17  import javax.jcr.RepositoryException;
18  import javax.jcr.Session;
19  import javax.jcr.Value;
20  import javax.jcr.version.Version;
21  import javax.jcr.version.VersionHistory;
22  import javax.jcr.version.VersionIterator;
23  import javax.jcr.version.VersionManager;
24  
25  import org.argeo.connect.ConnectException;
26  
27  /** History management */
28  public class VersionUtils {
29  
30  	// Filtered properties
31  	public static final List<String> DEFAULT_FILTERED_OUT_PROP_NAMES = asList("jcr:uuid", "jcr:frozenUuid",
32  			"jcr:frozenPrimaryType", "jcr:primaryType", "jcr:lastModified", "jcr:lastModifiedBy",
33  			Property.JCR_LAST_MODIFIED_BY);
34  
35  	public static List<VersionDiff> listHistoryDiff(Node entity, List<String> excludedProperties) {
36  		try {
37  			Session session = entity.getSession();
38  			List<VersionDiff> res = new ArrayList<VersionDiff>();
39  			VersionManager versionManager = session.getWorkspace().getVersionManager();
40  
41  			// if (!entity.hasProperty(Property.JCR_CREATED))
42  			// // Transient item. No history
43  			// return res;
44  
45  			VersionHistory versionHistory = null;
46  			try {
47  				versionHistory = versionManager.getVersionHistory(entity.getPath());
48  			} catch (Exception ise) {
49  				// TODO clean this:
50  				// Transient items that have just been created have no version
51  				// history
52  				// A jackrabbit specific NoSuchItemStateException is then
53  				// thrown.
54  				// We catch it and return an empty array
55  				return res;
56  			}
57  
58  			VersionIterator vit = versionHistory.getAllLinearVersions();
59  			// boolean first = true;
60  			while (vit.hasNext()) {
61  				Version version = vit.nextVersion();
62  				Node node = version.getFrozenNode();
63  
64  				Version predecessor = null;
65  				try {
66  					predecessor = version.getLinearPredecessor();
67  				} catch (Exception e) {
68  					// no predecessor throw an exception even if it shouldn't...
69  					// e.printStackTrace();
70  				}
71  				if (predecessor == null) {// original
72  				} else {
73  					Map<String, ItemDiff> diffs = VersionUtils.compareNodes(predecessor.getFrozenNode(), node,
74  							excludedProperties);
75  					if (!diffs.isEmpty()) {
76  						String userid = node.hasProperty(Property.JCR_LAST_MODIFIED_BY)
77  								? node.getProperty(Property.JCR_LAST_MODIFIED_BY).getString() : null;
78  						Calendar updateTime = node.hasProperty(Property.JCR_LAST_MODIFIED)
79  								? node.getProperty(Property.JCR_LAST_MODIFIED).getDate() : null;
80  						res.add(new VersionDiff(null, userid, updateTime, diffs));
81  					}
82  				}
83  			}
84  			return res;
85  		} catch (RepositoryException e) {
86  			throw new ConnectException("Cannot generate history for node " + entity, e);
87  		}
88  	}
89  
90  	/**
91  	 * Returns an ordered map of differences, either on node or on their
92  	 * properties
93  	 */
94  	public static Map<String, ItemDiff> compareNodes(Node reference, Node observed, List<String> excludedProperties) {
95  		// It is important to keep the same order
96  		Map<String, ItemDiff> diffs = new LinkedHashMap<String, ItemDiff>();
97  		compareNodes(diffs, null, reference, observed, excludedProperties);
98  		return diffs;
99  	}
100 
101 	/** Recursively compares 2 nodes */
102 	static void compareNodes(Map<String, ItemDiff> diffs, String relPath, Node reference, Node observed,
103 			List<String> excludedProperties) {
104 		Map<String, ItemDiff> localDiffs = new LinkedHashMap<String, ItemDiff>();
105 		try {
106 			compareProperties(localDiffs, relPath, reference, observed, excludedProperties);
107 
108 			// Removed and modified Node
109 			NodeIterator nit = reference.getNodes();
110 			while (nit.hasNext()) {
111 				Node n = nit.nextNode();
112 				String refRelPath = getRelPath(reference, n);
113 
114 				String currNodePath = (relPath != null ? relPath + "/" : "") + refRelPath;
115 				if (observed.hasNode(refRelPath)) {
116 					Map<String, ItemDiff> modDiffs = new LinkedHashMap<String, ItemDiff>();
117 					compareNodes(modDiffs, currNodePath, n, observed.getNode(refRelPath), excludedProperties);
118 					if (!modDiffs.isEmpty()) {
119 						ItemDiff iDiff = new ItemDiff(ItemDiff.MODIFIED, currNodePath, n, observed.getNode(refRelPath));
120 						localDiffs.put(currNodePath, iDiff);
121 						localDiffs.putAll(modDiffs);
122 					}
123 				} else {
124 					ItemDiff iDiff = new ItemDiff(ItemDiff.REMOVED, currNodePath, n, null);
125 					localDiffs.put(currNodePath, iDiff);
126 					addAllProperties(localDiffs, ItemDiff.REMOVED, true, n, excludedProperties);
127 				}
128 			}
129 			// Added nodes
130 			nit = observed.getNodes();
131 			while (nit.hasNext()) {
132 				Node n = nit.nextNode();
133 				String obsRelPath = getRelPath(observed, n);
134 				String currNodePath = (relPath != null ? relPath + "/" : "") + obsRelPath;
135 				if (!reference.hasNode(obsRelPath)) {
136 					ItemDiff iDiff = new ItemDiff(ItemDiff.ADDED, currNodePath, null, n);
137 					localDiffs.put(currNodePath, iDiff);
138 					// This triggers the display of duplicated properties when a
139 					// sub node is added. Violently commented out for the time
140 					// being.
141 					// TODO rework this
142 					// addAllProperties(localDiffs, ItemDiff.ADDED, true, n,
143 					// excludedProperties);
144 				}
145 			}
146 			// Modification found, we add them
147 			if (!localDiffs.isEmpty()) {
148 				// Small hack to avoid putting a line for parent node with only
149 				// a modification on their children
150 				// Typically, when we modify a contact, we don't want to have a
151 				// line for the people:contact parent
152 				if (localDiffs.size() >= 2) {
153 					Iterator<ItemDiff> it = localDiffs.values().iterator();
154 					if (isNodeDiff(it.next()) && isNodeDiff(it.next())) {
155 						// remove the first
156 						localDiffs.remove(localDiffs.keySet().iterator().next());
157 					}
158 				}
159 				diffs.putAll(localDiffs);
160 			}
161 		} catch (RepositoryException e) {
162 			throw new ConnectException("Cannot diff " + reference + " and " + observed, e);
163 		}
164 	}
165 
166 	static public boolean isNodeDiff(ItemDiff diff) {
167 		Item refItem = diff.getReferenceItem();
168 		Item newItem = diff.getObservedItem();
169 		Item tmpItem = refItem == null ? newItem : refItem;
170 		return tmpItem instanceof Node;
171 	}
172 
173 	static void addAllProperties(Map<String, ItemDiff> diffs, Integer type, boolean trackSubNode, Node node,
174 			List<String> excludedProperties) throws RepositoryException {
175 		PropertyIterator pit = node.getProperties();
176 		props: while (pit.hasNext()) {
177 			Property p = pit.nextProperty();
178 			String name = p.getName();
179 			if (excludedProperties.contains(name))
180 				continue props;
181 			ItemDiff iDiff;
182 			if (ItemDiff.ADDED == type)
183 				iDiff = new ItemDiff(ItemDiff.ADDED, name, null, p);
184 			else
185 				iDiff = new ItemDiff(ItemDiff.REMOVED, name, p, null);
186 			diffs.put(name, iDiff);
187 		}
188 
189 		// TODO: for the time being, we do not yet add sub nodes and their
190 		// properties in the diff
191 		// Investigate: We might have to also deal and update this to correctly
192 		// managed added rel pathes.
193 
194 		// NodeIterator nit = node.getNodes();
195 		// nodes: while (nit.hasNext()) {
196 		// Node n = nit.nextNode();
197 		// String name = n.getName();
198 		// // TODO add nodes to ignore in the property list
199 		// if (excludedProperties.contains(name))
200 		// continue nodes;
201 		// ItemDiff iDiff;
202 		// if (ItemDiff.ADDED == type){
203 		// iDiff = new ItemDiff(ItemDiff.ADDED, name, null, n);
204 		// localDiffs.put(currNodePath, iDiff);
205 		// addAllProperties(localDiffs, ItemDiff.ADDED, n,
206 		// excludedProperties);
207 		//
208 		// localDiffs, ItemDiff.ADDED, n,
209 		// excludedProperties
210 		// }
211 		// else
212 		// iDiff = new ItemDiff(ItemDiff.REMOVED, name, p, null);
213 		// diffs.put(name, iDiff);
214 		// }
215 		//
216 
217 	}
218 
219 	/**
220 	 * Compare the properties of two nodes. Extends
221 	 * <code>JcrUtils.diffPropertiesLevel</code> to also track differences in
222 	 * multiple value properties and sub graph. No property is skipped (among
223 	 * other all technical jcr:... properties) to be able to track jcr:title and
224 	 * description properties, among other. Filtering must be applied afterwards
225 	 * to only keep relevant properties.
226 	 */
227 	static void compareProperties(Map<String, ItemDiff> diffs, String relPath, Node reference, Node observed,
228 			List<String> excludedProperties) {
229 		try {
230 			// Removed and modified properties
231 			PropertyIterator pit = reference.getProperties();
232 			props: while (pit.hasNext()) {
233 				Property p = pit.nextProperty();
234 				String name = p.getName();
235 				String relName = propertyRelPath(relPath, name);
236 				if (excludedProperties.contains(name))
237 					continue props;
238 				if (!observed.hasProperty(name)) {
239 					ItemDiff iDiff = new ItemDiff(ItemDiff.REMOVED, name, p, null);
240 					diffs.put(relName, iDiff);
241 				} else {
242 					if (p.isMultiple()) {
243 
244 						Value[] refValues = p.getValues();
245 						Value[] newValues = observed.getProperty(name).getValues();
246 						refValues: for (Value refValue : refValues) {
247 							for (Value newValue : newValues) {
248 								if (refValue.equals(newValue))
249 									continue refValues;
250 							}
251 							// At least one value has been removed -> modified
252 							// prop
253 							ItemDiff iDiff = new ItemDiff(ItemDiff.MODIFIED, name, p, observed.getProperty(name));
254 							diffs.put(relName, iDiff);
255 							continue props;
256 						}
257 
258 						newValues: for (Value newValue : newValues) {
259 							for (Value refValue : refValues) {
260 								if (refValue.equals(newValue))
261 									continue newValues;
262 							}
263 							// At least one value has been added -> modified
264 							// prop
265 							ItemDiff iDiff = new ItemDiff(ItemDiff.MODIFIED, name, p, observed.getProperty(name));
266 							diffs.put(relName, iDiff);
267 							continue props;
268 						}
269 					} else {
270 						Value referenceValue = p.getValue();
271 						Value newValue = observed.getProperty(name).getValue();
272 						if (!referenceValue.equals(newValue)) {
273 							ItemDiff iDiff = new ItemDiff(ItemDiff.MODIFIED, name, p, observed.getProperty(name));
274 							diffs.put(relName, iDiff);
275 						}
276 					}
277 				}
278 			}
279 			// Added properties
280 			pit = observed.getProperties();
281 			props: while (pit.hasNext()) {
282 				Property p = pit.nextProperty();
283 				String name = p.getName();
284 				String relName = propertyRelPath(relPath, name);
285 				if (excludedProperties.contains(name))
286 					continue props;
287 				if (!reference.hasProperty(name)) {
288 					ItemDiff pDiff = new ItemDiff(ItemDiff.ADDED, name, null, p);
289 					diffs.put(relName, pDiff);
290 				}
291 			}
292 		} catch (RepositoryException e) {
293 			throw new ConnectException("Cannot diff " + reference + " and " + observed, e);
294 		}
295 	}
296 
297 	private static String getRelPath(Node parent, Node descendant) throws RepositoryException {
298 		String pPath = parent.getPath();
299 		String dPath = descendant.getPath();
300 		if (!dPath.startsWith(pPath))
301 			throw new ConnectException(
302 					"Cannot get rel path for " + descendant + ". It is not a descendant of " + parent);
303 		String relPath = dPath.substring(pPath.length());
304 		if (relPath.startsWith("/"))
305 			relPath = relPath.substring(1);
306 		return relPath;
307 	}
308 
309 	/** Builds a property relPath to be used in the diff map. */
310 	private static String propertyRelPath(String baseRelPath, String propertyName) {
311 		if (baseRelPath == null)
312 			return propertyName;
313 		else
314 			return baseRelPath + '/' + propertyName;
315 	}
316 }