]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java
Add JCR MVC
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / JcrUtils.java
1 /*
2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.argeo.jcr;
18
19 import java.text.DateFormat;
20 import java.text.ParseException;
21 import java.util.Calendar;
22 import java.util.Date;
23 import java.util.GregorianCalendar;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.StringTokenizer;
28 import java.util.TreeMap;
29
30 import javax.jcr.NamespaceRegistry;
31 import javax.jcr.Node;
32 import javax.jcr.NodeIterator;
33 import javax.jcr.Property;
34 import javax.jcr.PropertyIterator;
35 import javax.jcr.RepositoryException;
36 import javax.jcr.Session;
37 import javax.jcr.Value;
38 import javax.jcr.nodetype.NodeType;
39 import javax.jcr.query.Query;
40 import javax.jcr.query.QueryResult;
41
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44 import org.argeo.ArgeoException;
45
46 /** Utility methods to simplify common JCR operations. */
47 public class JcrUtils {
48 private final static Log log = LogFactory.getLog(JcrUtils.class);
49
50 /**
51 * Queries one single node.
52 *
53 * @return one single node or null if none was found
54 * @throws ArgeoException
55 * if more than one node was found
56 */
57 public static Node querySingleNode(Query query) {
58 NodeIterator nodeIterator;
59 try {
60 QueryResult queryResult = query.execute();
61 nodeIterator = queryResult.getNodes();
62 } catch (RepositoryException e) {
63 throw new ArgeoException("Cannot execute query " + query, e);
64 }
65 Node node;
66 if (nodeIterator.hasNext())
67 node = nodeIterator.nextNode();
68 else
69 return null;
70
71 if (nodeIterator.hasNext())
72 throw new ArgeoException("Query returned more than one node.");
73 return node;
74 }
75
76 /** Removes forbidden characters from a path, replacing them with '_' */
77 public static String removeForbiddenCharacters(String str) {
78 return str.replace('[', '_').replace(']', '_').replace('/', '_')
79 .replace('*', '_');
80
81 }
82
83 /** Retrieves the parent path of the provided path */
84 public static String parentPath(String path) {
85 if (path.equals("/"))
86 throw new ArgeoException("Root path '/' has no parent path");
87 if (path.charAt(0) != '/')
88 throw new ArgeoException("Path " + path + " must start with a '/'");
89 String pathT = path;
90 if (pathT.charAt(pathT.length() - 1) == '/')
91 pathT = pathT.substring(0, pathT.length() - 2);
92
93 int index = pathT.lastIndexOf('/');
94 return pathT.substring(0, index);
95 }
96
97 /** The provided data as a path ('/' at the end, not the beginning) */
98 public static String dateAsPath(Calendar cal) {
99 return dateAsPath(cal, false);
100 }
101
102 /**
103 * The provided data as a path ('/' at the end, not the beginning)
104 *
105 * @param cal
106 * the date
107 * @param addHour
108 * whether to add hour as well
109 */
110 public static String dateAsPath(Calendar cal, Boolean addHour) {
111 StringBuffer buf = new StringBuffer(14);
112 buf.append('Y').append(cal.get(Calendar.YEAR));// 5
113 buf.append('/');// 1
114 int month = cal.get(Calendar.MONTH) + 1;
115 buf.append('M');
116 if (month < 10)
117 buf.append(0);
118 buf.append(month);// 3
119 buf.append('/');// 1
120 int day = cal.get(Calendar.DAY_OF_MONTH);
121 if (day < 10)
122 buf.append(0);
123 buf.append('D').append(day);// 3
124 buf.append('/');// 1
125 if (addHour) {
126 int hour = cal.get(Calendar.HOUR_OF_DAY);
127 if (hour < 10)
128 buf.append(0);
129 buf.append('H').append(hour);// 3
130 buf.append('/');// 1
131 }
132 return buf.toString();
133
134 }
135
136 /** Converts in one call a string into a gregorian calendar. */
137 public static Calendar parseCalendar(DateFormat dateFormat, String value) {
138 try {
139 Date date = dateFormat.parse(value);
140 Calendar calendar = new GregorianCalendar();
141 calendar.setTime(date);
142 return calendar;
143 } catch (ParseException e) {
144 throw new ArgeoException("Cannot parse " + value
145 + " with date format " + dateFormat, e);
146 }
147
148 }
149
150 /** Converts the FQDN of an host into a path (converts '.' into '/'). */
151 public static String hostAsPath(String host) {
152 // TODO : inverse order of the elements (to have org/argeo/test IO
153 // test/argeo/org
154 return host.replace('.', '/');
155 }
156
157 /** The last element of a path. */
158 public static String lastPathElement(String path) {
159 if (path.charAt(path.length() - 1) == '/')
160 throw new ArgeoException("Path " + path + " cannot end with '/'");
161 int index = path.lastIndexOf('/');
162 if (index < 0)
163 throw new ArgeoException("Cannot find last path element for "
164 + path);
165 return path.substring(index + 1);
166 }
167
168 /** Creates the nodes making path, if they don't exist. */
169 public static Node mkdirs(Session session, String path) {
170 return mkdirs(session, path, null, false);
171 }
172
173 /** Creates the nodes making path, if they don't exist. */
174 public static Node mkdirs(Session session, String path, String type,
175 Boolean versioning) {
176 try {
177 if (path.equals('/'))
178 return session.getRootNode();
179
180 if (session.itemExists(path)) {
181 Node node = session.getNode(path);
182 // check type
183 if (type != null
184 && !type.equals(node.getPrimaryNodeType().getName()))
185 throw new ArgeoException("Node " + node
186 + " exists but is of type "
187 + node.getPrimaryNodeType().getName()
188 + " not of type " + type);
189 // TODO: check versioning
190 return node;
191 }
192
193 StringTokenizer st = new StringTokenizer(path, "/");
194 StringBuffer current = new StringBuffer("/");
195 Node currentNode = session.getRootNode();
196 while (st.hasMoreTokens()) {
197 String part = st.nextToken();
198 current.append(part).append('/');
199 if (!session.itemExists(current.toString())) {
200 if (type != null)
201 currentNode = currentNode.addNode(part, type);
202 else
203 currentNode = currentNode.addNode(part);
204 if (versioning)
205 currentNode.addMixin(NodeType.MIX_VERSIONABLE);
206 if (log.isTraceEnabled())
207 log.debug("Added folder " + part + " as " + current);
208 } else {
209 currentNode = (Node) session.getItem(current.toString());
210 }
211 }
212 session.save();
213 return currentNode;
214 } catch (RepositoryException e) {
215 throw new ArgeoException("Cannot mkdirs " + path, e);
216 }
217 }
218
219 /**
220 * Safe and repository implementation independent registration of a
221 * namespace.
222 */
223 public static void registerNamespaceSafely(Session session, String prefix,
224 String uri) {
225 try {
226 registerNamespaceSafely(session.getWorkspace()
227 .getNamespaceRegistry(), prefix, uri);
228 } catch (RepositoryException e) {
229 throw new ArgeoException("Cannot find namespace registry", e);
230 }
231 }
232
233 /**
234 * Safe and repository implementation independent registration of a
235 * namespace.
236 */
237 public static void registerNamespaceSafely(NamespaceRegistry nr,
238 String prefix, String uri) {
239 try {
240 String[] prefixes = nr.getPrefixes();
241 for (String pref : prefixes)
242 if (pref.equals(prefix)) {
243 String registeredUri = nr.getURI(pref);
244 if (!registeredUri.equals(uri))
245 throw new ArgeoException("Prefix " + pref
246 + " already registered for URI "
247 + registeredUri
248 + " which is different from provided URI "
249 + uri);
250 else
251 return;// skip
252 }
253 nr.registerNamespace(prefix, uri);
254 } catch (RepositoryException e) {
255 throw new ArgeoException("Cannot register namespace " + uri
256 + " under prefix " + prefix, e);
257 }
258 }
259
260 /** Recursively outputs the contents of the given node. */
261 public static void debug(Node node) {
262 try {
263 // First output the node path
264 log.debug(node.getPath());
265 // Skip the virtual (and large!) jcr:system subtree
266 if (node.getName().equals("jcr:system")) {
267 return;
268 }
269
270 // Then the children nodes (recursive)
271 NodeIterator it = node.getNodes();
272 while (it.hasNext()) {
273 Node childNode = it.nextNode();
274 debug(childNode);
275 }
276
277 // Then output the properties
278 PropertyIterator properties = node.getProperties();
279 // log.debug("Property are : ");
280
281 while (properties.hasNext()) {
282 Property property = properties.nextProperty();
283 if (property.getDefinition().isMultiple()) {
284 // A multi-valued property, print all values
285 Value[] values = property.getValues();
286 for (int i = 0; i < values.length; i++) {
287 log.debug(property.getPath() + "="
288 + values[i].getString());
289 }
290 } else {
291 // A single-valued property
292 log.debug(property.getPath() + "=" + property.getString());
293 }
294 }
295 } catch (Exception e) {
296 log.error("Could not debug " + node, e);
297 }
298
299 }
300
301 /**
302 * Copies recursively the content of a node to another one. Mixin are NOT
303 * copied.
304 */
305 public static void copy(Node fromNode, Node toNode) {
306 try {
307 PropertyIterator pit = fromNode.getProperties();
308 properties: while (pit.hasNext()) {
309 Property fromProperty = pit.nextProperty();
310 String propertyName = fromProperty.getName();
311 if (toNode.hasProperty(propertyName)
312 && toNode.getProperty(propertyName).getDefinition()
313 .isProtected())
314 continue properties;
315
316 toNode.setProperty(fromProperty.getName(),
317 fromProperty.getValue());
318 }
319
320 NodeIterator nit = fromNode.getNodes();
321 while (nit.hasNext()) {
322 Node fromChild = nit.nextNode();
323 Integer index = fromChild.getIndex();
324 String nodeRelPath = fromChild.getName() + "[" + index + "]";
325 Node toChild;
326 if (toNode.hasNode(nodeRelPath))
327 toChild = toNode.getNode(nodeRelPath);
328 else
329 toChild = toNode.addNode(fromChild.getName(), fromChild
330 .getPrimaryNodeType().getName());
331 copy(fromChild, toChild);
332 }
333 } catch (RepositoryException e) {
334 throw new ArgeoException("Cannot copy " + fromNode + " to "
335 + toNode, e);
336 }
337 }
338
339 /**
340 * Check whether all first-level properties (except jcr:* properties) are
341 * equal. Skip jcr:* properties
342 */
343 public static Boolean allPropertiesEquals(Node reference, Node observed,
344 Boolean onlyCommonProperties) {
345 try {
346 PropertyIterator pit = reference.getProperties();
347 props: while (pit.hasNext()) {
348 Property propReference = pit.nextProperty();
349 String propName = propReference.getName();
350 if (propName.startsWith("jcr:"))
351 continue props;
352
353 if (!observed.hasProperty(propName))
354 if (onlyCommonProperties)
355 continue props;
356 else
357 return false;
358 // TODO: deal with multiple property values?
359 if (!observed.getProperty(propName).getValue()
360 .equals(propReference.getValue()))
361 return false;
362 }
363 return true;
364 } catch (RepositoryException e) {
365 throw new ArgeoException("Cannot check all properties equals of "
366 + reference + " and " + observed, e);
367 }
368 }
369
370 public static Map<String, PropertyDiff> diffProperties(Node reference,
371 Node observed) {
372 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
373 diffPropertiesLevel(diffs, null, reference, observed);
374 return diffs;
375 }
376
377 /**
378 * Compare the properties of two nodes. Recursivity to child nodes is not
379 * yet supported. Skip jcr:* properties.
380 */
381 static void diffPropertiesLevel(Map<String, PropertyDiff> diffs,
382 String baseRelPath, Node reference, Node observed) {
383 try {
384 // check removed and modified
385 PropertyIterator pit = reference.getProperties();
386 props: while (pit.hasNext()) {
387 Property p = pit.nextProperty();
388 String name = p.getName();
389 if (name.startsWith("jcr:"))
390 continue props;
391
392 if (!observed.hasProperty(name)) {
393 String relPath = propertyRelPath(baseRelPath, name);
394 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
395 relPath, p.getValue(), null);
396 diffs.put(relPath, pDiff);
397 } else {
398 if (p.isMultiple())
399 continue props;
400 Value referenceValue = p.getValue();
401 Value newValue = observed.getProperty(name).getValue();
402 if (!referenceValue.equals(newValue)) {
403 String relPath = propertyRelPath(baseRelPath, name);
404 PropertyDiff pDiff = new PropertyDiff(
405 PropertyDiff.MODIFIED, relPath, referenceValue,
406 newValue);
407 diffs.put(relPath, pDiff);
408 }
409 }
410 }
411 // check added
412 pit = observed.getProperties();
413 props: while (pit.hasNext()) {
414 Property p = pit.nextProperty();
415 String name = p.getName();
416 if (name.startsWith("jcr:"))
417 continue props;
418 if (!reference.hasProperty(name)) {
419 String relPath = propertyRelPath(baseRelPath, name);
420 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
421 relPath, null, p.getValue());
422 diffs.put(relPath, pDiff);
423 }
424 }
425 } catch (RepositoryException e) {
426 throw new ArgeoException("Cannot diff " + reference + " and "
427 + observed, e);
428 }
429 }
430
431 /**
432 * Compare only a restricted list of properties of two nodes. No
433 * recursivity.
434 *
435 */
436 public static Map<String, PropertyDiff> diffProperties(Node reference,
437 Node observed, List<String> properties) {
438 Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
439 try {
440 Iterator<String> pit = properties.iterator();
441
442 props: while (pit.hasNext()) {
443 String name = pit.next();
444 if (!reference.hasProperty(name)) {
445 if (!observed.hasProperty(name))
446 continue props;
447 Value val = observed.getProperty(name).getValue();
448 try {
449 // empty String but not null
450 if ("".equals(val.getString()))
451 continue props;
452 } catch (Exception e) {
453 // not parseable as String, silent
454 }
455 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED,
456 name, null, val);
457 diffs.put(name, pDiff);
458 } else if (!observed.hasProperty(name)) {
459 PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED,
460 name, reference.getProperty(name).getValue(), null);
461 diffs.put(name, pDiff);
462 } else {
463 Value referenceValue = reference.getProperty(name)
464 .getValue();
465 Value newValue = observed.getProperty(name).getValue();
466 if (!referenceValue.equals(newValue)) {
467 PropertyDiff pDiff = new PropertyDiff(
468 PropertyDiff.MODIFIED, name, referenceValue,
469 newValue);
470 diffs.put(name, pDiff);
471 }
472 }
473 }
474 } catch (RepositoryException e) {
475 throw new ArgeoException("Cannot diff " + reference + " and "
476 + observed, e);
477 }
478 return diffs;
479 }
480
481 /** Builds a property relPath to be used in the diff. */
482 private static String propertyRelPath(String baseRelPath,
483 String propertyName) {
484 if (baseRelPath == null)
485 return propertyName;
486 else
487 return baseRelPath + '/' + propertyName;
488 }
489 }