View Javadoc
1   package org.argeo.connect.util;
2   
3   import java.math.BigDecimal;
4   import java.security.AccessControlException;
5   import java.text.SimpleDateFormat;
6   import java.util.ArrayList;
7   import java.util.Calendar;
8   import java.util.GregorianCalendar;
9   import java.util.List;
10  
11  import javax.jcr.InvalidItemStateException;
12  import javax.jcr.ItemNotFoundException;
13  import javax.jcr.Node;
14  import javax.jcr.NodeIterator;
15  import javax.jcr.Property;
16  import javax.jcr.PropertyType;
17  import javax.jcr.Repository;
18  import javax.jcr.RepositoryException;
19  import javax.jcr.Session;
20  import javax.jcr.Value;
21  import javax.jcr.ValueFactory;
22  import javax.jcr.nodetype.NodeType;
23  import javax.jcr.query.Query;
24  import javax.jcr.query.QueryManager;
25  import javax.jcr.query.QueryResult;
26  import javax.jcr.query.Row;
27  import javax.jcr.query.RowIterator;
28  import javax.jcr.query.qom.Constraint;
29  import javax.jcr.query.qom.QueryObjectModelFactory;
30  import javax.jcr.query.qom.Selector;
31  import javax.jcr.query.qom.StaticOperand;
32  import javax.jcr.version.VersionManager;
33  
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.argeo.connect.ConnectConstants;
37  import org.argeo.connect.ConnectException;
38  import org.argeo.eclipse.ui.EclipseUiUtils;
39  import org.argeo.jcr.JcrUtils;
40  
41  /**
42   * Utility methods to ease interaction with a JCR repository while implementing.
43   * This might move to commons in a near future
44   */
45  public class ConnectJcrUtils {
46  	private final static Log log = LogFactory.getLog(ConnectJcrUtils.class);
47  
48  	private final static String NS_JCR = "http://www.jcp.org/jcr/1.0";
49  	private final static String NS_NT = "http://www.jcp.org/jcr/nt/1.0";
50  
51  	/**
52  	 * Replace the generic namespace with the local "jcr:" value. It is a workaround
53  	 * that must be later cleaned
54  	 * 
55  	 * @param name
56  	 *            the property name which prefix has to be cleaned
57  	 * @return the short property name
58  	 */
59  	public static String getLocalJcrItemName(String name) {
60  		String jcr = "{" + NS_JCR + "}";
61  		String nt = "{" + NS_NT + "}";
62  		if (name.startsWith(jcr))
63  			return "jcr:" + name.substring(jcr.length());
64  		else if (name.startsWith(nt))
65  			return "nt:" + name.substring(nt.length());
66  		else
67  			throw new ConnectException("Unknown prefix for " + name);
68  	}
69  
70  	public static String checkAndLocalizeNamespaces(String name) {
71  		String jcr = "{" + NS_JCR + "}";
72  		String nt = "{" + NS_NT + "}";
73  		if (name.startsWith(jcr))
74  			return "jcr:" + name.substring(jcr.length());
75  		else if (name.startsWith(nt))
76  			return "nt:" + name.substring(nt.length());
77  		else
78  			return name;
79  	}
80  
81  	public static String cleanNodeName(String name) {
82  		String cleanName = name.trim().replaceAll("[^a-zA-Z0-9-. _]", "");
83  		return cleanName;
84  	}
85  
86  	/**
87  	 * Add '?' to the list of forbidden characters. See
88  	 * JcrUtils.replaceInvalidChars(String name)
89  	 */
90  	public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', '?', /*
91  																								 * invalid XML chars :
92  																								 */
93  			'<', '>', '&' };
94  
95  	/**
96  	 * Replaces characters which are invalid in a JCR name by '_'. Currently not
97  	 * exhaustive.
98  	 * 
99  	 * @see JcrUtils#INVALID_NAME_CHARACTERS
100 	 */
101 	public static String replaceInvalidChars(String name) {
102 		return replaceInvalidChars(name, '_');
103 	}
104 
105 	/**
106 	 * Replaces characters which are invalid in a JCR name. Currently not
107 	 * exhaustive.
108 	 * 
109 	 * @see JcrUtils#INVALID_NAME_CHARACTERS
110 	 */
111 	public static String replaceInvalidChars(String name, char replacement) {
112 		boolean modified = false;
113 		char[] arr = name.toCharArray();
114 		for (int i = 0; i < arr.length; i++) {
115 			char c = arr[i];
116 			invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
117 				if (c == invalid) {
118 					arr[i] = replacement;
119 					modified = true;
120 					break invalid;
121 				}
122 			}
123 		}
124 		if (modified)
125 			return new String(arr);
126 		else
127 			// do not create new object if unnecessary
128 			return name;
129 	}
130 
131 	// PATH MANAGEMENT
132 	/** Simply retrieves the parent rel path of the provided relative path */
133 	public static String parentRelPath(String relPath) {
134 		// Remove trailing slash
135 		if (relPath.charAt(relPath.length() - 1) == '/')
136 			relPath = relPath.substring(0, relPath.length() - 2);
137 
138 		int index = relPath.lastIndexOf('/');
139 		if (index < 0)
140 			return "";
141 		else
142 			return relPath.substring(0, index);
143 	}
144 
145 	/** The last element of a relative path. */
146 	public static String lastRelPathElement(String relPath) {
147 		// Remove trailing slash
148 		if (relPath.charAt(relPath.length() - 1) == '/')
149 			relPath = relPath.substring(0, relPath.length() - 2);
150 
151 		int index = relPath.lastIndexOf('/');
152 		if (index < 0)
153 			return relPath;
154 		return relPath.substring(index + 1);
155 	}
156 
157 	public static boolean canEdit(Node entity) {
158 		boolean canEdit = false;
159 		try {
160 			entity.getSession().checkPermission(entity.getPath(), "add_node");
161 			canEdit = true;
162 		} catch (AccessControlException ace) {
163 			// silent
164 		} catch (RepositoryException e) {
165 			throw new ConnectException("Unable to check permission on " + entity, e);
166 		}
167 		return canEdit;
168 	}
169 
170 	/**
171 	 * Helper for label provider: returns the Node if element is a Node or retrieves
172 	 * the Node if the object is a row. Expects a single Node in the row if no
173 	 * selector name is provided Call {@link Row#getNode()} catching
174 	 * {@link RepositoryException}
175 	 */
176 	public static Node getNodeFromElement(Object element, String selectorName) {
177 		Node currNode;
178 		if (element instanceof Row) {
179 			Row currRow = (Row) element;
180 			try {
181 				if (selectorName != null)
182 					currNode = currRow.getNode(selectorName);
183 				else
184 					currNode = currRow.getNode();
185 			} catch (RepositoryException re) {
186 				throw new ConnectException(
187 						"Unable to retrieve Node with selector name " + selectorName + " on " + currRow, re);
188 			}
189 		} else if (element instanceof Node)
190 			currNode = (Node) element;
191 		else
192 			throw new ConnectException("unsupported element type: " + element.getClass().getName());
193 		return currNode;
194 	}
195 
196 	/**
197 	 * Returns the versionable node in the parent path, this if it is versionable or
198 	 * null if none is versionnable including root node.
199 	 */
200 	public static Node getVersionableAncestor(Node node) {
201 		try {
202 			if (node.isNodeType(NodeType.MIX_VERSIONABLE)) // "mix:versionable"
203 				return node;
204 			else if (node.getPath().equals("/"))
205 				return null;
206 			else
207 				return getVersionableAncestor(node.getParent());
208 		} catch (RepositoryException re) {
209 			throw new ConnectException("Unable to get check out status for node " + node, re);
210 		}
211 	}
212 
213 	/**
214 	 * Works around missing method to test if a node has been removed from existing
215 	 * session
216 	 * 
217 	 * @param node
218 	 * @return
219 	 */
220 	public static boolean nodeStillExists(Node node) {
221 		try {
222 			node.getPath();
223 		} catch (InvalidItemStateException iise) {
224 			return false;
225 		} catch (RepositoryException re) {
226 			throw new ConnectException("Error while testing node existence", re);
227 		}
228 		return true;
229 	}
230 
231 	/**
232 	 * Wraps the versionMananger.isCheckedOut(path) method to adapt it to the
233 	 * current check in / check out policy.
234 	 * 
235 	 * TODO : add management of check out by others.
236 	 */
237 	public static boolean isNodeCheckedOut(Node node) {
238 		try {
239 			if (!node.isNodeType(NodeType.MIX_VERSIONABLE))
240 				return true;
241 			else
242 				return node.getSession().getWorkspace().getVersionManager().isCheckedOut(node.getPath());
243 		} catch (RepositoryException re) {
244 			throw new ConnectException("Unable to get check out status for node " + node, re);
245 		}
246 	}
247 
248 	/**
249 	 * Shortcut to get a node iterator on all nodes of a given type under a given
250 	 * subpath.
251 	 */
252 	public static NodeIterator getNodesOfType(Session session, String parentPath, String nodeType) {
253 		try {
254 			if (parentPath == null)
255 				parentPath = "/";
256 			StringBuilder builder = new StringBuilder();
257 			builder.append(XPathUtils.descendantFrom(parentPath));
258 			builder.append("//element(*, ").append(nodeType).append(")");
259 			Query query = session.getWorkspace().getQueryManager().createQuery(builder.toString(),
260 					ConnectConstants.QUERY_XPATH);
261 			return query.execute().getNodes();
262 		} catch (RepositoryException re) {
263 			throw new ConnectException("Unable to retrieve node of type " + nodeType + " under " + parentPath, re);
264 		}
265 	}
266 
267 	public static Node getByPropertyValue(Session session, String parentPath, String nodeType, String propName,
268 			String propValue) {
269 		NodeIterator nit = getByPropertyValue(session, parentPath, nodeType, propName, propValue, false);
270 		if (nit != null && nit.hasNext())
271 			return nit.nextNode();
272 		else
273 			return null;
274 	}
275 
276 	public static NodeIterator getByPropertyValue(Session session, String parentPath, String nodeType, String propName,
277 			String propValue, boolean acceptMultipleResult) {
278 		try {
279 			QueryManager queryManager = session.getWorkspace().getQueryManager();
280 			String xpathQueryStr = XPathUtils.descendantFrom(parentPath) + "//element(*, "
281 					+ checkAndLocalizeNamespaces(nodeType) + ")";
282 
283 			String attrQuery = XPathUtils.getPropertyEquals(checkAndLocalizeNamespaces(propName), propValue);
284 
285 			xpathQueryStr += "[" + attrQuery + "]";
286 			// String cleanStr = cleanStatement(xpathQueryStr);
287 
288 			Query xpathQuery = queryManager.createQuery(xpathQueryStr, ConnectConstants.QUERY_XPATH);
289 			QueryResult result = xpathQuery.execute();
290 			NodeIterator ni = result.getNodes();
291 
292 			long niSize = ni.getSize();
293 			if (niSize == 0)
294 				return null;
295 			else if (niSize > 1) {
296 				if (acceptMultipleResult)
297 					return ni;
298 				else
299 					throw new ConnectException("Found " + niSize + " entities of type " + nodeType + "with " + propName
300 							+ " =[" + propValue + "] under " + parentPath);
301 			} else
302 				return ni;
303 		} catch (RepositoryException e) {
304 			throw new ConnectException("Unable to retrieve entities of type " + nodeType + " with " + propName + " = ["
305 					+ propValue + "] under " + parentPath, e);
306 		}
307 	}
308 
309 	/** Retrieves the human readable label of a property */
310 	public static String getPropertyTypeAsString(Property prop) {
311 		try {
312 			return PropertyType.nameFromValue(prop.getType());
313 		} catch (RepositoryException e) {
314 			throw new ConnectException("Cannot check type for " + prop, e);
315 		}
316 	}
317 
318 	/**
319 	 * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it updates
320 	 * the {@link Property#JCR_LAST_MODIFIED} property with the current time and the
321 	 * {@link Property#JCR_LAST_MODIFIED_BY} property with the passed user id. In
322 	 * Jackrabbit 2.x,
323 	 * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
324 	 * not automatically updated</a>, hence the need for manual update. The session
325 	 * is not saved.
326 	 */
327 	public static void updateLastModified(Node node, String userId) {
328 		try {
329 			if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
330 				node.addMixin(NodeType.MIX_LAST_MODIFIED);
331 			node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar());
332 			node.setProperty(Property.JCR_LAST_MODIFIED_BY, userId);
333 		} catch (RepositoryException e) {
334 			throw new ConnectException("Cannot update last modified on " + node, e);
335 		}
336 	}
337 
338 	/* VERSIONING MANAGEMENT */
339 	/**
340 	 * For the time being, same as isNodeCheckedOut(Node node). TODO : add
341 	 * management of check out by others.
342 	 */
343 	public static boolean isNodeCheckedOutByMe(Node node) {
344 		return isNodeCheckedOut(node);
345 	}
346 
347 	/**
348 	 * Make a version snapshot of the current state of the given versionable node.
349 	 * It wraps a JCR save and checkPoint methods
350 	 */
351 	public static boolean saveAndPublish(Node node, boolean publish) {
352 		try {
353 			boolean changed = false;
354 			Session session = node.getSession();
355 			if (session.hasPendingChanges()) {
356 				JcrUtils.updateLastModified(node);
357 				session.save();
358 				changed = true;
359 			}
360 			if (isVersionable(node) && publish) {
361 				VersionManager vm = session.getWorkspace().getVersionManager();
362 				String path = node.getPath();
363 				vm.checkpoint(path);
364 			} else if (publish && !isVersionable(node)) {
365 				log.warn("Cannot publish unversionnable node at " + node.getPath());
366 			}
367 			return changed;
368 		} catch (RepositoryException re) {
369 			throw new ConnectException("Unable to perform check point on " + node, re);
370 		}
371 	}
372 
373 	/**
374 	 * Shortcut to save the underlying session if it has pending changes without
375 	 * exception
376 	 */
377 	public static boolean saveIfNecessary(Node node) {
378 		boolean changed = false;
379 		try {
380 			Session session = node.getSession();
381 			if (session.hasPendingChanges()) {
382 				session.save();
383 				changed = true;
384 			}
385 			return changed;
386 		} catch (RepositoryException re) {
387 			throw new ConnectException("Cannot save with node " + node, re);
388 		}
389 	}
390 
391 	/**
392 	 * Wraps a best effort to versionMananger.checkedPoint(path) a list of path. We
393 	 * check if the node still exists because the list might be out-dated
394 	 * 
395 	 * We assume the session has been saved.
396 	 *
397 	 * Not that are not versionable won't be touched TODO : add management of check
398 	 * out by others.
399 	 */
400 	public static void checkPoint(Session session, List<String> pathes, boolean updateLastModified) {
401 		try {
402 			VersionManager vm = session.getWorkspace().getVersionManager();
403 			loop: for (String currPath : pathes) {
404 				if (!session.nodeExists(currPath))
405 					continue loop;
406 				try {
407 					Node currNode = session.getNode(currPath);
408 					if (!currNode.isNodeType(NodeType.MIX_VERSIONABLE))
409 						continue loop;
410 
411 					if (updateLastModified) {
412 						JcrUtils.updateLastModified(currNode);
413 						session.save();
414 					}
415 					vm.checkpoint(currPath);
416 				} catch (RepositoryException re) {
417 					throw new ConnectException("Unable to perform check point on " + currPath, re);
418 				}
419 			}
420 		} catch (RepositoryException re) {
421 			throw new ConnectException("Unexpected error when performing batch check point ", re);
422 		}
423 	}
424 
425 	/**
426 	 * Simplify the save strategy keeping the check-in status unchanged. Goes
427 	 * together with <code>checkCOStatusAfterUpdate</code>
428 	 */
429 	public static boolean checkCOStatusBeforeUpdate(Node node) {
430 		boolean wasCheckedOut = isNodeCheckedOutByMe(node);
431 		if (!wasCheckedOut)
432 			checkout(node);
433 		return wasCheckedOut;
434 	}
435 
436 	/**
437 	 * Simply retrieves the first versionable node in the current node ancestor tree
438 	 * (might be the ndoe itself) or null if none of them is versionable
439 	 */
440 	public static Node getParentVersionableNode(Node node) throws RepositoryException {
441 		Node curr = node;
442 		while (true) {
443 			if (curr.isNodeType(NodeType.MIX_VERSIONABLE))
444 				return curr;
445 			try {
446 				curr = curr.getParent();
447 			} catch (ItemNotFoundException infe) {
448 				// root node
449 				return null;
450 			}
451 		}
452 	}
453 
454 	// ENCAPSULATE COMMONS JCR CALLS
455 	// with the try/catch block to simplify simple UI code
456 	/**
457 	 * Call {@link Repository#login()} without exceptions (useful in super
458 	 * constructors).
459 	 */
460 	public static Session login(Repository repository) {
461 		try {
462 			return repository.login();
463 		} catch (RepositoryException re) {
464 			throw new ConnectException("Unable to login", re);
465 		}
466 	}
467 
468 	/** Centralises exception management to call {@link Node#getSession()} */
469 	public static Session getSession(Node node) {
470 		try {
471 			return node.getSession();
472 		} catch (RepositoryException re) {
473 			throw new ConnectException("Unable to retrieve session for node " + node, re);
474 		}
475 	}
476 
477 	/**
478 	 * Centralises exception management to call
479 	 * {@link Node#getSession()#getRepository()}
480 	 */
481 	public static Repository getRepository(Node node) {
482 		try {
483 			return node.getSession().getRepository();
484 		} catch (RepositoryException re) {
485 			throw new ConnectException("Unable to retrieve repository for node " + node, re);
486 		}
487 	}
488 
489 	/** Centralises exception management to call {@link Node#getIdentifier()} */
490 	public static String getIdentifier(Node node) {
491 		try {
492 			return node.getIdentifier();
493 		} catch (RepositoryException re) {
494 			throw new ConnectException("Unable to retrieve identifier for node " + node, re);
495 		}
496 	}
497 
498 	/** Centralises exception management to call {@link Node#getName()} */
499 	public static String getName(Node node) {
500 		try {
501 			return node.getName();
502 		} catch (RepositoryException re) {
503 			throw new ConnectException("Unable to retrieve name for node " + node, re);
504 		}
505 
506 	}
507 
508 	/** Centralises exception management to call {@link Node#getPath()} */
509 	public static String getPath(Node node) {
510 		try {
511 			return node.getPath();
512 		} catch (RepositoryException re) {
513 			throw new ConnectException("Unable to retrieve path for node " + node, re);
514 		}
515 	}
516 
517 	/** Shortcut to manage case where parentPath is "/" (parent is root) */
518 	public static String getAbsPath(String parPath, String nodeName) {
519 		String absPath = null;
520 		if ("/".equals(parPath))
521 			absPath = "/" + nodeName;
522 		else
523 			absPath = parPath + "/" + nodeName;
524 		return absPath;
525 	}
526 
527 	/** Simply calls {@link Session#itemExists(String)} with no try/catch */
528 	public static boolean itemExists(Session session, String absPath) {
529 		try {
530 			return session.itemExists(absPath);
531 		} catch (RepositoryException re) {
532 			throw new ConnectException("Unable to check existence of item at " + absPath, re);
533 		}
534 	}
535 
536 	/**
537 	 * Centralizes exception management to call
538 	 * {@link Session#getNodeByIdentifier(String)}
539 	 */
540 	public static Node getNodeByIdentifier(Session session, String identifier) {
541 		try {
542 			return session.getNodeByIdentifier(identifier);
543 		} catch (RepositoryException re) {
544 			throw new ConnectException("Unable to retrieve node by identifier with " + identifier, re);
545 		}
546 	}
547 
548 	/** Centralizes exception management to call {@link Node#getParent()} */
549 	public static Node getParent(Node child) {
550 		try {
551 			return child.getParent();
552 		} catch (RepositoryException re) {
553 			throw new ConnectException("Unable to retrieve parent node for " + child, re);
554 		}
555 	}
556 
557 	/**
558 	 * Centralizes exception management to call
559 	 * {@link Session#getNodeByIdentifier(String)}. The session is retrieved using
560 	 * the passed node
561 	 */
562 	public static Node getNodeByIdentifier(Node sessionNode, String identifier) {
563 		return getNodeByIdentifier(getSession(sessionNode), identifier);
564 	}
565 
566 	/**
567 	 * Call {@link Session#getNode()} catching {@link RepositoryException}
568 	 */
569 	public static Node getNode(Session session, String absPath) {
570 		try {
571 			return session.getNode(absPath);
572 		} catch (RepositoryException re) {
573 			throw new ConnectException("Unable to retrieve Node at path " + absPath, re);
574 		}
575 	}
576 
577 	/**
578 	 * If session.absParentPath exists and is visible, build a relative path with
579 	 * the passed relative subpath and returns the node at corresponding relative
580 	 * path if it exists
581 	 * 
582 	 * Call {@link Session#getNode()} catching {@link RepositoryException}
583 	 */
584 	public static Node getNode(Session session, String absParentPath, String... childRelPath) {
585 		try {
586 			if (EclipseUiUtils.isEmpty(absParentPath))
587 				absParentPath = "/";
588 			if (!session.itemExists(absParentPath))
589 				throw new ConnectException("Node at " + absParentPath + " does not exist or is not visible");
590 			StringBuilder builder = new StringBuilder();
591 			for (String rp : childRelPath)
592 				builder.append(rp).append("/");
593 			String relPath = builder.substring(0, builder.length() - 1);
594 			Node parent = session.getNode(absParentPath);
595 			if (parent.hasNode(relPath))
596 				return parent.getNode(relPath);
597 			else
598 				return null;
599 		} catch (RepositoryException re) {
600 			throw new ConnectException(
601 					"Unable to retrieve Node at path " + absParentPath + " with child rel path " + childRelPath, re);
602 		}
603 	}
604 
605 	/**
606 	 * Call {@link Row#getNode()} catching {@link RepositoryException}
607 	 */
608 	public static Node getNode(Row row, String selectorName) {
609 		try {
610 			if (selectorName == null)
611 				return row.getNode();
612 			else
613 				return row.getNode(selectorName);
614 		} catch (RepositoryException re) {
615 			throw new ConnectException("Unable to retrieve Node with selector name " + selectorName + " on " + row, re);
616 		}
617 	}
618 
619 	/** Calls {@link Node#isNodetype(String)} without exceptions */
620 	public static boolean isNodeType(Node node, String nodeTypeName) {
621 		try {
622 			return node.isNodeType(nodeTypeName);
623 		} catch (RepositoryException re) {
624 			throw new ConnectException("Unable to test NodeType " + nodeTypeName + " for " + node, re);
625 		}
626 	}
627 
628 	/** Simply retrieves primary node type name */
629 	public static String getPrimaryNodeType(Node node) {
630 		try {
631 			return node.getPrimaryNodeType().getName();
632 		} catch (RepositoryException re) {
633 			throw new ConnectException("Unable to retrieve node type name for " + node, re);
634 		}
635 	}
636 
637 	/** Concisely check out a node. */
638 	private static void checkout(Node node) {
639 		try {
640 			node.getSession().getWorkspace().getVersionManager().checkout(node.getPath());
641 		} catch (RepositoryException re) {
642 			throw new ConnectException("Unable to check out node  " + node, re);
643 		}
644 	}
645 
646 	/** Simply check if a node is versionable */
647 	public static boolean isVersionable(Node node) {
648 		try {
649 			return node.isNodeType(NodeType.MIX_VERSIONABLE);
650 		} catch (RepositoryException re) {
651 			throw new ConnectException("Unable to test versionability  of " + node, re);
652 		}
653 	}
654 
655 	/* HELPERS FOR SINGLE VALUES */
656 	/**
657 	 * Concisely gets the String value of a property. Returns an empty String rather
658 	 * than null if this node doesn't have this property or if the corresponding
659 	 * property is an empty string.
660 	 */
661 	public static String get(Node node, String propertyName) {
662 		try {
663 			if (!node.hasProperty(propertyName))
664 				return "";
665 			else
666 				return node.getProperty(propertyName).getString();
667 		} catch (RepositoryException e) {
668 			throw new ConnectException("Cannot get property " + propertyName + " of " + node, e);
669 		}
670 	}
671 
672 	/**
673 	 * Concisely gets the value of a long property or null if this node doesn't have
674 	 * this property
675 	 */
676 	public static Long getLongValue(Node node, String propRelPath) {
677 		try {
678 			if (!node.hasProperty(propRelPath))
679 				return null;
680 			else
681 				return node.getProperty(propRelPath).getLong();
682 		} catch (RepositoryException e) {
683 			throw new ConnectException("Cannot get long property " + propRelPath + " of " + node, e);
684 		}
685 	}
686 
687 	/**
688 	 * Concisely gets the value of a double property or null if this node doesn't
689 	 * have this property
690 	 */
691 	public static Double getDoubleValue(Node node, String propRelPath) {
692 		try {
693 			if (!node.hasProperty(propRelPath))
694 				return null;
695 			else
696 				return node.getProperty(propRelPath).getDouble();
697 		} catch (RepositoryException e) {
698 			throw new ConnectException("Cannot get double property " + propRelPath + " of " + node, e);
699 		}
700 	}
701 
702 	/**
703 	 * Concisely gets the value of a date property or null if this node doesn't have
704 	 * this property
705 	 */
706 	public static Calendar getDateValue(Node node, String propRelPath) {
707 		try {
708 			if (!node.hasProperty(propRelPath))
709 				return null;
710 			else
711 				return node.getProperty(propRelPath).getDate();
712 		} catch (RepositoryException e) {
713 			throw new ConnectException("Cannot get date property " + propRelPath + " of " + node, e);
714 		}
715 	}
716 
717 	/**
718 	 * Concisely gets the value of a boolean property or null if this node doesn't
719 	 * have this property
720 	 */
721 	public static Boolean getBooleanValue(Node node, String propertyName) {
722 		try {
723 			if (!node.hasProperty(propertyName))
724 				return null;
725 			else
726 				return node.getProperty(propertyName).getBoolean();
727 		} catch (RepositoryException e) {
728 			throw new ConnectException("Cannot get boolean property " + propertyName + " of " + node, e);
729 		}
730 	}
731 
732 	/**
733 	 * Concisely gets the value of a date property formatted as String or an empty
734 	 * String this node doesn't have this property
735 	 */
736 	public static String getDateFormattedAsString(Node node, String propertyName, String dateFormatPattern) {
737 		try {
738 			if (!node.hasProperty(propertyName))
739 				return null;
740 			else {
741 				Calendar cal = node.getProperty(propertyName).getDate();
742 				return new SimpleDateFormat(dateFormatPattern).format(cal.getTime());
743 			}
744 		} catch (RepositoryException e) {
745 			throw new ConnectException("Cannot get date property " + propertyName + " on " + node, e);
746 		}
747 	}
748 
749 	/**
750 	 * Concisely gets a referenced node or null if the given node doesn't have this
751 	 * property or if the property is of the wrong type
752 	 */
753 	public static Node getReference(Node node, String propName) {
754 		try {
755 			Node ref = null;
756 			if (node.hasProperty(propName)) {
757 				Property prop = node.getProperty(propName);
758 				if (prop.getType() == PropertyType.REFERENCE) {
759 					ref = prop.getNode();
760 				}
761 			}
762 			return ref;
763 		} catch (RepositoryException re) {
764 			throw new ConnectException("Unable to get reference " + propName + " for node " + node, re);
765 		}
766 	}
767 
768 	/**
769 	 * Centralises management of updating property value. Among other to avoid
770 	 * infinite loop when the new value is the same as the one that is already
771 	 * stored in JCR (typically in UI Text controls that have a listener).
772 	 * 
773 	 * @return true if the value as changed
774 	 */
775 	public static boolean setJcrProperty(Node node, String propName, int propertyType, Object value) {
776 		try {
777 			// Manage null value
778 			if (value == null)
779 				if (node.hasProperty(propName)) {
780 					node.getProperty(propName).remove();
781 					return true;
782 				} else
783 					return false;
784 
785 			switch (propertyType) {
786 			case PropertyType.STRING:
787 				String strValue = (String) value;
788 				if (node.hasProperty(propName) && node.getProperty(propName).getString().equals(strValue))
789 					return false;
790 				else {
791 					node.setProperty(propName, strValue);
792 					return true;
793 				}
794 			case PropertyType.REFERENCE:
795 			case PropertyType.WEAKREFERENCE:
796 				Node nodeValue = (Node) value;
797 				if (node.hasProperty(propName)
798 						&& nodeValue.getIdentifier().equals(node.getProperty(propName).getNode().getIdentifier()))
799 					return false;
800 				else {
801 					node.setProperty(propName, nodeValue);
802 					return true;
803 				}
804 			case PropertyType.BOOLEAN:
805 				if (node.hasProperty(propName) && node.getProperty(propName).getBoolean() == (Boolean) value)
806 					return false;
807 				else {
808 					node.setProperty(propName, (Boolean) value);
809 					return true;
810 				}
811 			case PropertyType.DATE:
812 				if (node.hasProperty(propName) && node.getProperty(propName).getDate().equals((Calendar) value))
813 					return false;
814 				else {
815 					node.setProperty(propName, (Calendar) value);
816 					return true;
817 				}
818 			case PropertyType.LONG:
819 				Long lgValue = (Long) value;
820 				if (node.hasProperty(propName) && node.getProperty(propName).getLong() == lgValue)
821 					return false;
822 				else {
823 					node.setProperty(propName, lgValue);
824 					return true;
825 				}
826 			case PropertyType.DOUBLE:
827 				Double dbValue;
828 				if (value instanceof Double)
829 					dbValue = (Double) value;
830 				else {
831 					try {
832 						dbValue = Double.parseDouble(value.toString());
833 					} catch (NumberFormatException e) {
834 						return false;
835 					}
836 				}
837 				if (node.hasProperty(propName) && node.getProperty(propName).getDouble() == dbValue)
838 					return false;
839 				else {
840 					node.setProperty(propName, dbValue);
841 					return true;
842 				}
843 			case PropertyType.DECIMAL:
844 				BigDecimal bdValue = (BigDecimal) value;
845 				if (node.hasProperty(propName) && node.getProperty(propName).getDecimal() == bdValue)
846 					return false;
847 				else {
848 					node.setProperty(propName, bdValue);
849 					return true;
850 				}
851 			default:
852 				throw new ConnectException("Update unimplemented for property type " + propertyType
853 						+ ". Unable to update property " + propName + " on " + node);
854 			}
855 		} catch (RepositoryException re) {
856 			throw new ConnectException("Unexpected error while setting property " + propName + " on " + node, re);
857 		}
858 	}
859 
860 	/* MULTIPLE VALUES MANAGEMENT */
861 	/**
862 	 * Removes a given String from a multi value property of a node. If the String
863 	 * is not found, it does nothing
864 	 */
865 	public static void removeMultiPropertyValue(Node node, String propName, String stringToRemove) {
866 		try {
867 			boolean foundValue = false;
868 
869 			List<String> strings = new ArrayList<String>();
870 			Value[] values = node.getProperty(propName).getValues();
871 			for (int i = 0; i < values.length; i++) {
872 				String curr = values[i].getString();
873 				if (stringToRemove.equals(curr))
874 					foundValue = true;
875 				else
876 					strings.add(curr);
877 			}
878 			if (foundValue)
879 				node.setProperty(propName, strings.toArray(new String[0]));
880 		} catch (RepositoryException e) {
881 			throw new ConnectException(
882 					"Unable to remove value " + stringToRemove + " for property " + propName + " of " + node, e);
883 		}
884 	}
885 
886 	/**
887 	 * Adds a string value on a multivalued property. If this value is already part
888 	 * of the list, it returns an error message. We use case insensitive comparison
889 	 */
890 	public static String addMultiPropertyValue(Node node, String propName, String value) {
891 		try {
892 			Value[] values;
893 			String[] valuesStr;
894 			String errMsg = null;
895 			if (node.hasProperty(propName)) {
896 				values = node.getProperty(propName).getValues();
897 
898 				// Check duplicate
899 				for (Value currVal : values) {
900 					String curTagUpperCase = currVal.getString().toUpperCase().trim();
901 					if (value.toUpperCase().trim().equals(curTagUpperCase)) {
902 						errMsg = value + " is already in the list and thus could not be added.";
903 						return errMsg;
904 					}
905 				}
906 				valuesStr = new String[values.length + 1];
907 				int i;
908 				for (i = 0; i < values.length; i++) {
909 					valuesStr[i] = values[i].getString();
910 				}
911 				valuesStr[i] = value;
912 			} else {
913 				valuesStr = new String[1];
914 				valuesStr[0] = value;
915 			}
916 			node.setProperty(propName, valuesStr);
917 			return null;
918 		} catch (RepositoryException re) {
919 			throw new ConnectException("Unable to set tags", re);
920 		}
921 	}
922 
923 	/**
924 	 * Add a string value on a multivalued property. WARNING if values is not an
925 	 * empty String, it overrides any existing value, and delete old ones.
926 	 */
927 	public static void setMultiValueStringPropFromString(Node node, String propName, String values, String separator) {
928 		try {
929 			if (notEmpty(values)) {
930 				String[] valArray = values.split(separator);
931 				// Remove any empty value
932 				List<String> newValList = new ArrayList<String>();
933 				for (String currValue : valArray) {
934 					if (notEmpty(currValue))
935 						newValList.add(currValue);
936 				}
937 				node.setProperty(propName, newValList.toArray(new String[0]));
938 			}
939 		} catch (RepositoryException re) {
940 			throw new ConnectException("Unable to set multi value property " + propName + " of node " + node
941 					+ " with values [" + values + "]", re);
942 		}
943 	}
944 
945 	/**
946 	 * Concisely gets a string that concatenates values of a multi-valued String
947 	 * property. It returns an empty String rather than null if this node doesn't
948 	 * have this property or if the corresponding property is an empty string.
949 	 * 
950 	 * Useful in the read only label providers. Caller might define a concatenation
951 	 * string, otherwise a semi-colon and a space are used.
952 	 */
953 	public static String getMultiAsString(Node node, String propertyName, String separator) {
954 		try {
955 			if (separator == null)
956 				separator = "; ";
957 			if (!node.hasProperty(propertyName))
958 				return "";
959 			else {
960 				Value[] values = node.getProperty(propertyName).getValues();
961 				StringBuilder builder = new StringBuilder();
962 				for (Value val : values) {
963 					String currStr = val.getString();
964 					if (notEmpty(currStr))
965 						builder.append(currStr).append(separator);
966 				}
967 				if (builder.lastIndexOf(separator) >= 0)
968 					return builder.substring(0, builder.length() - separator.length());
969 				else
970 					return builder.toString();
971 			}
972 		} catch (RepositoryException e) {
973 			throw new ConnectException("Cannot get multi valued property " + propertyName + " of " + node, e);
974 		}
975 	}
976 
977 	/**
978 	 * Concisely gets a list with the values of a multi-valued String property.
979 	 * Returns an empty list if the property does not exist.
980 	 */
981 	public static List<String> getMultiAsList(Node node, String propertyName) {
982 		List<String> results = new ArrayList<String>();
983 		try {
984 			if (propertyName == null)
985 				return results;
986 			if (!node.hasProperty(propertyName))
987 				return results;
988 			else {
989 				Value[] values = node.getProperty(propertyName).getValues();
990 				for (Value val : values) {
991 					results.add(val.getString());
992 				}
993 			}
994 		} catch (RepositoryException e) {
995 			throw new ConnectException("Cannot get multi valued property " + propertyName + " of " + node, e);
996 		}
997 		return results;
998 	}
999 
1000 	/**
1001 	 * Sets a property of type REFERENCE that is multiple. Overrides any already
1002 	 * defined value of this property
1003 	 */
1004 	public static void setMultipleReferences(Node node, String propertyName, List<Node> nodes)
1005 			throws RepositoryException {
1006 		ValueFactory vFactory = node.getSession().getValueFactory();
1007 		int size = nodes.size();
1008 		Value[] values = new Value[size];
1009 		int i = 0;
1010 		for (Node currNode : nodes) {
1011 			Value val = vFactory.createValue(currNode.getIdentifier(), PropertyType.REFERENCE);
1012 			values[i++] = val;
1013 		}
1014 		node.setProperty(propertyName, values);
1015 	}
1016 
1017 	/** Remove a Reference from a multi valued property */
1018 	public static void removeRefFromMultiValuedProp(Node node, String propName, String identifier) {
1019 		try {
1020 			Session session = node.getSession();
1021 			List<Node> nodes = new ArrayList<Node>();
1022 			Value[] values = node.getProperty(propName).getValues();
1023 			for (int i = 0; i < values.length; i++) {
1024 				String curr = values[i].getString();
1025 				if (!identifier.equals(curr))
1026 					nodes.add(session.getNodeByIdentifier(curr));
1027 			}
1028 			setMultipleReferences(node, propName, nodes);
1029 		} catch (RepositoryException e) {
1030 			throw new ConnectException("Unable to remove reference from property " + propName + " of Node " + node, e);
1031 		}
1032 	}
1033 
1034 	/**
1035 	 * Adds a reference to a JCR Node to the multi valued REFERENCE property of a
1036 	 * Node. An error message is returned if the Node is already referenced. The new
1037 	 * reference is always added after all already existing references
1038 	 * 
1039 	 * TODO rather use exception when trying to add an already referenced node TODO
1040 	 * Enable definition of a primary item by adding the new property as first
1041 	 * element in the list
1042 	 */
1043 	public static String addRefToMultiValuedProp(Node node, String propName, Node nodeToReference) {
1044 		try {
1045 			Session session = node.getSession();
1046 			Value[] values;
1047 			List<Node> nodes = new ArrayList<Node>();
1048 			String errMsg = null;
1049 			if (node.hasProperty(propName)) {
1050 				values = node.getProperty(propName).getValues();
1051 				// Check duplicate
1052 				for (Value currValue : values) {
1053 					String jcrId = currValue.getString();
1054 					if (nodeToReference.getIdentifier().equals(jcrId)) {
1055 						errMsg = ConnectJcrUtils.get(nodeToReference, Property.JCR_TITLE)
1056 								+ " is already in the list and thus could not be added.";
1057 						return errMsg;
1058 					} else
1059 						nodes.add(session.getNodeByIdentifier(jcrId));
1060 				}
1061 			}
1062 			nodes.add(nodeToReference);
1063 			setMultipleReferences(node, propName, nodes);
1064 			return null;
1065 		} catch (RepositoryException re) {
1066 			throw new ConnectException("Unable to add reference ", re);
1067 		}
1068 	}
1069 
1070 	/**
1071 	 * Insert a reference to a given node in a multi value reference property just
1072 	 * before the reference that is passed as target parameter. Usefull among other
1073 	 * in the UI drag and drop mechanisms. If the target reference is not found, the
1074 	 * new reference is added at the end of the list. This mechanism also check if
1075 	 * another occurence of the source reference is present and remove it
1076 	 */
1077 	public static void orderReferenceBefore(Node node, String propName, Node sourceNode, Node targetNode) {
1078 		try {
1079 			Session session = node.getSession();
1080 			String sourceId = sourceNode.getIdentifier();
1081 			String targetId = null;
1082 			if (targetNode != null)
1083 				targetId = targetNode.getIdentifier();
1084 
1085 			Value[] values;
1086 			List<Node> nodes = new ArrayList<Node>();
1087 			if (node.hasProperty(propName)) {
1088 				values = node.getProperty(propName).getValues();
1089 				// Check duplicate
1090 				for (Value currValue : values) {
1091 					String jcrId = currValue.getString();
1092 					if (sourceId.equals(jcrId)) {
1093 						// does not add
1094 					} else if (jcrId.equals(targetId)) {
1095 						nodes.add(session.getNodeByIdentifier(sourceId));
1096 						nodes.add(session.getNodeByIdentifier(targetId));
1097 					} else
1098 						nodes.add(session.getNodeByIdentifier(jcrId));
1099 				}
1100 				if (targetId == null)
1101 					nodes.add(session.getNodeByIdentifier(sourceId));
1102 			}
1103 			setMultipleReferences(node, propName, nodes);
1104 		} catch (RepositoryException re) {
1105 			throw new ConnectException("Unable to update node " + node + " to order " + sourceNode + " before "
1106 					+ targetNode + " in multi value reference property " + propName, re);
1107 		}
1108 	}
1109 
1110 	/**
1111 	 * Simply checks a multi valued STRING property of a Node and returns true if
1112 	 * the given property has already such a value. comparison is case insensitive
1113 	 * and trimmed.
1114 	 */
1115 	public static boolean valueExists(Node node, String propName, String value) {
1116 		try {
1117 			value = value.trim().toLowerCase();
1118 			if (node.hasProperty(propName)) {
1119 				Value[] values = node.getProperty(propName).getValues();
1120 				for (Value currVal : values) {
1121 					String currStr = currVal.getString().trim().toLowerCase();
1122 					if (value.equals(currStr))
1123 						return true;
1124 				}
1125 			}
1126 			return false;
1127 		} catch (RepositoryException re) {
1128 			throw new ConnectException("Unable to set tags", re);
1129 		}
1130 	}
1131 
1132 	/**
1133 	 * Adds a String to the multi valued STRING property of a Node. An error message
1134 	 * is returned if the String is already in the list. The new String is always
1135 	 * added after all already existing Strings.
1136 	 * 
1137 	 * TODO rather use exception when trying to add an already existing String TODO
1138 	 * Enable definition of a primary item by adding the new property as first
1139 	 * element in the list
1140 	 */
1141 	public static String addStringToMultiValuedProp(Node node, String propName, String value) {
1142 		try {
1143 			Value[] values;
1144 			String[] valuesStr;
1145 			String errMsg = null;
1146 			if (node.hasProperty(propName)) {
1147 				values = node.getProperty(propName).getValues();
1148 
1149 				// Check duplicate
1150 				for (Value currVal : values) {
1151 					String currStr = currVal.getString();
1152 					if (value.equals(currStr)) {
1153 						errMsg = value + " is already in the list and thus " + "could not be added.";
1154 						return errMsg;
1155 					}
1156 				}
1157 
1158 				valuesStr = new String[values.length + 1];
1159 				int i;
1160 				for (i = 0; i < values.length; i++) {
1161 					valuesStr[i] = values[i].getString();
1162 				}
1163 				valuesStr[i] = value;
1164 			} else {
1165 				valuesStr = new String[1];
1166 				valuesStr[0] = value;
1167 			}
1168 			node.setProperty(propName, valuesStr);
1169 			return null;
1170 		} catch (RepositoryException re) {
1171 			throw new ConnectException("Unable to set tags", re);
1172 		}
1173 	}
1174 
1175 	/** Remove a String from a multi valued property */
1176 	public static void removeStringFromMultiValuedProp(Node node, String propName, String value) {
1177 		try {
1178 			if (node.hasProperty(propName)) {
1179 				List<Value> nodes = new ArrayList<Value>();
1180 				Value[] values = node.getProperty(propName).getValues();
1181 				for (int i = 0; i < values.length; i++) {
1182 					String curr = values[i].getString();
1183 					if (!value.equals(curr))
1184 						nodes.add(values[i]);
1185 				}
1186 				Value[] results = nodes.toArray(new Value[0]);
1187 				node.setProperty(propName, results);
1188 			}
1189 		} catch (RepositoryException e) {
1190 			throw new ConnectException("Unable to remove reference from property " + propName + " of Node " + node, e);
1191 		}
1192 	}
1193 
1194 	/* MISCELLANEOUS */
1195 
1196 	/* WIDELY USED PATTERNS */
1197 	/** Browses a {@code NodeIterator} to build the corresponding Node array. */
1198 	public static Node[] nodeIteratorToArray(NodeIterator nit) {
1199 		Node[] nodes = new Node[(int) nit.getSize()];
1200 		int i = 0;
1201 		while (nit.hasNext()) {
1202 			nodes[i++] = nit.nextNode();
1203 		}
1204 		return nodes;
1205 	}
1206 
1207 	/** Browses a {@code RowIterator} to build the corresponding row array. */
1208 	public static Row[] rowIteratorToArray(RowIterator rit) {
1209 		List<Row> rows = new ArrayList<Row>();
1210 		while (rit.hasNext()) {
1211 			rows.add(rit.nextRow());
1212 		}
1213 		return rows.toArray(new Row[rows.size()]);
1214 	}
1215 
1216 	/**
1217 	 * Browses a {@code RowIterator} to build the corresponding row array. Performs
1218 	 * a kind of "select distinct" based on the JcrUID of the nodes designed by the
1219 	 * selector name
1220 	 */
1221 	public static Row[] rowIteratorToDistinctArray(RowIterator rit, String distinctSelectorName)
1222 			throws RepositoryException {
1223 		List<Row> rows = new ArrayList<Row>();
1224 		List<String> jcrIds = new ArrayList<String>();
1225 		while (rit.hasNext()) {
1226 			Row curr = rit.nextRow();
1227 			String currId = curr.getNode(distinctSelectorName).getIdentifier();
1228 			if (jcrIds.contains(currId))
1229 				; // skip it
1230 			else {
1231 				jcrIds.add(currId);
1232 				rows.add(curr);
1233 			}
1234 		}
1235 		return rows.toArray(new Row[rows.size()]);
1236 	}
1237 
1238 	/**
1239 	 * Convert a {@link RowIterator} to a list of {@link Node} given a selector
1240 	 * name. It relies on the &lt;code&gt;Row.getNode(String
1241 	 * selectorName)&lt;/code&gt; method.
1242 	 */
1243 	public static List<Node> rowIteratorToNodeList(RowIterator rowIterator, String selectorName)
1244 			throws RepositoryException {
1245 		List<Node> nodes = new ArrayList<Node>();
1246 		while (rowIterator.hasNext()) {
1247 			Row row = rowIterator.nextRow();
1248 			if (row.getNode(selectorName) != null)
1249 				nodes.add(row.getNode(selectorName));
1250 		}
1251 		return nodes;
1252 	}
1253 
1254 	/** Parses and trims a String of values */
1255 	public static String[] parseAndClean(String string, String regExp, boolean clean) {
1256 		String[] temp = string.split(regExp);
1257 		if (clean) {
1258 			String[] cleanRes = new String[temp.length];
1259 			int i = 0;
1260 			for (String tag : temp) {
1261 				cleanRes[i] = tag.trim();
1262 				i++;
1263 			}
1264 			return cleanRes;
1265 		}
1266 		return temp;
1267 	}
1268 
1269 	/** Concatenates 2 strings with given separator if they are not empty */
1270 	public static String concatIfNotEmpty(String str1, String str2, String separator) {
1271 		StringBuilder builder = new StringBuilder();
1272 		if (notEmpty(str1))
1273 			builder.append(str1);
1274 
1275 		if (notEmpty(str1) && notEmpty(str2))
1276 			builder.append(separator);
1277 
1278 		if (notEmpty(str2))
1279 			builder.append(str2);
1280 		return builder.toString();
1281 	}
1282 
1283 	/* QOM HELPERS */
1284 	/**
1285 	 * Returns and(constraintA, constraintB) if constraintA != null, or constraintB
1286 	 * otherwise (that cannot be null)
1287 	 */
1288 	public static Constraint localAnd(QueryObjectModelFactory factory, Constraint defaultC, Constraint newC)
1289 			throws RepositoryException {
1290 		if (defaultC == null)
1291 			return newC;
1292 		else
1293 			return factory.and(defaultC, newC);
1294 	}
1295 
1296 	/** Widely used pattern in various UI Parts */
1297 	public static Constraint getFreeTextConstraint(Session session, QueryObjectModelFactory factory, Selector source,
1298 			String filter) throws RepositoryException {
1299 		Constraint defaultC = null;
1300 		if (notEmpty(filter)) {
1301 			String[] strs = filter.trim().split(" ");
1302 			for (String token : strs) {
1303 				StaticOperand so = factory.literal(session.getValueFactory().createValue("*" + token + "*"));
1304 				Constraint currC = factory.fullTextSearch(source.getSelectorName(), null, so);
1305 				defaultC = localAnd(factory, defaultC, currC);
1306 			}
1307 		}
1308 		return defaultC;
1309 	}
1310 
1311 	/* HELPERS */
1312 	static boolean notEmpty(String stringToTest) {
1313 		return !(stringToTest == null || "".equals(stringToTest.trim()));
1314 	}
1315 
1316 	/* PREVENTS INSTANTIATION */
1317 	private ConnectJcrUtils() {
1318 	}
1319 }