From: Mathieu Baudier Date: Tue, 3 Nov 2020 07:45:06 +0000 (+0100) Subject: Introduce JCRX checksum and improve JCR last modified routines. X-Git-Tag: argeo-commons-2.1.89~41 X-Git-Url: http://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=a2590cf3e2ad039f004f13ef6c97a9f702841e5b Introduce JCRX checksum and improve JCR last modified routines. --- diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java index 3ae2036ae..8f0685280 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java @@ -142,7 +142,7 @@ public class FormPageViewer extends AbstractPageViewer { // TODO: make this configurable, sometimes we do not want to save the // current session at this stage if (node != null && node.getSession().hasPendingChanges()) { - JcrUtils.updateLastModified(node); + JcrUtils.updateLastModified(node, true); node.getSession().save(); } } diff --git a/org.argeo.jcr/src/org/argeo/jcr/Jcr.java b/org.argeo.jcr/src/org/argeo/jcr/Jcr.java index fe73b53f4..263128448 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/Jcr.java +++ b/org.argeo.jcr/src/org/argeo/jcr/Jcr.java @@ -344,7 +344,8 @@ public class Jcr { } /** - * Get property as a {@link String}. + * Get property as a {@link String}. If the property is multiple it returns the + * first value. * * @return the value of * {@link Node#getProperty(String)}.{@link Property#getString()} or @@ -353,9 +354,18 @@ public class Jcr { */ public static String get(Node node, String property, String defaultValue) { try { - if (node.hasProperty(property)) - return node.getProperty(property).getString(); - else + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + if (!p.isMultiple()) + return p.getString(); + else { + Value[] values = p.getValues(); + if (values.length == 0) + return defaultValue; + else + return values[0].getString(); + } + } else return defaultValue; } catch (RepositoryException e) { throw new IllegalStateException("Cannot retrieve property " + property + " from " + node); @@ -392,6 +402,7 @@ public class Jcr { @SuppressWarnings("unchecked") public static T getAs(Node node, String property, T defaultValue) { try { + // TODO deal with multiple if (node.hasProperty(property)) { Property p = node.getProperty(property); try { diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java b/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java index 9dfbf305a..98dce7eb8 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java +++ b/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java @@ -13,6 +13,7 @@ import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.text.DateFormat; import java.text.ParseException; +import java.time.Instant; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -754,7 +755,7 @@ public class JcrUtils { // update jcr:lastModified and jcr:lastModifiedBy in toNode in case // they existed, before adding the mixins - updateLastModified(toNode); + updateLastModified(toNode, true); // add mixins for (NodeType mixinType : fromNode.getMixinNodeTypes()) { @@ -1168,17 +1169,62 @@ public class JcrUtils { } /** - * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it updates - * the {@link Property#JCR_LAST_MODIFIED} property with the current time and the - * {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying session - * user id. In Jackrabbit 2.x, + * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards) + * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}. + */ + public static Instant getModified(Node node) { + Calendar calendar = null; + try { + if (node.hasProperty(Property.JCR_LAST_MODIFIED)) + calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate(); + else if (node.hasProperty(Property.JCR_CREATED)) + calendar = node.getProperty(Property.JCR_CREATED).getDate(); + else + throw new IllegalArgumentException("No modification time found in " + node); + return calendar.toInstant(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get modification time for " + node, e); + } + + } + + /** + * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set. + */ + public static Instant getCreated(Node node) { + Calendar calendar = null; + try { + if (node.hasProperty(Property.JCR_CREATED)) + calendar = node.getProperty(Property.JCR_CREATED).getDate(); + else + throw new IllegalArgumentException("No created time found in " + node); + return calendar.toInstant(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get created time for " + node, e); + } + + } + + /** + * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time + * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying + * session user id. + */ + public static void updateLastModified(Node node) { + updateLastModified(node, false); + } + + /** + * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time + * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying + * session user id. In Jackrabbit 2.x, * these properties are * not automatically updated, hence the need for manual update. The session * is not saved. */ - public static void updateLastModified(Node node) { + public static void updateLastModified(Node node, boolean addMixin) { try { - if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED)) + if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED)) node.addMixin(NodeType.MIX_LAST_MODIFIED); node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar()); node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID()); @@ -1193,17 +1239,17 @@ public class JcrUtils { * @param node the node * @param untilPath the base path, null is equivalent to "/" */ - public static void updateLastModifiedAndParents(Node node, String untilPath) { + public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) { try { if (untilPath != null && !node.getPath().startsWith(untilPath)) throw new IllegalArgumentException(node + " is not under " + untilPath); - updateLastModified(node); + updateLastModified(node, addMixin); if (untilPath == null) { if (!node.getPath().equals("/")) - updateLastModifiedAndParents(node.getParent(), untilPath); + updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); } else { if (!node.getPath().equals(untilPath)) - updateLastModifiedAndParents(node.getParent(), untilPath); + updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); } } catch (RepositoryException e) { throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e); diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java new file mode 100644 index 000000000..ab34c730e --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java @@ -0,0 +1,161 @@ +package org.argeo.jcr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** Uilities around the JCR extensions. */ +public class JcrxApi { + public final static String MD5 = "MD5"; + public final static String SHA1 = "SHA1"; + public final static String SHA256 = "SHA-256"; + public final static String SHA512 = "SHA-512"; + + public final static String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e"; + public final static String EMPTY_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + public final static String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + public final static String EMPTY_SHA512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"; + + public final static int LENGTH_MD5 = EMPTY_MD5.length(); + public final static int LENGTH_SHA1 = EMPTY_SHA1.length(); + public final static int LENGTH_SHA256 = EMPTY_SHA256.length(); + public final static int LENGTH_SHA512 = EMPTY_SHA512.length(); + + /* + * XML + */ + /** + * Set as a subnode which will be exported as an XML element. + */ + public static String getXmlValue(Node node, String name) { + try { + if (!node.hasNode(name)) + throw new IllegalArgumentException("No XML text named " + name); + return node.getNode(name).getNode(Jcr.JCR_XMLTEXT).getProperty(Jcr.JCR_XMLCHARACTERS).getString(); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get " + name + " as XML text", e); + } + } + + /** + * Set as a subnode which will be exported as an XML element. + */ + public static void setXmlValue(Node node, String name, String value) { + try { + if (node.hasNode(name)) + node.getNode(name).getNode(Jcr.JCR_XMLTEXT).setProperty(Jcr.JCR_XMLCHARACTERS, value); + else + node.addNode(name, JcrxType.JCRX_XMLVALUE).addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT) + .setProperty(Jcr.JCR_XMLCHARACTERS, value); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot set " + name + " as XML text", e); + } + } + + /** + * Add a checksum replacing the one which was previously set with the same + * length. + */ + public static void addChecksum(Node node, String checksum) { + try { + if (!node.hasProperty(JcrxName.JCRX_SUM)) { + node.setProperty(JcrxName.JCRX_SUM, new String[] { checksum }); + return; + } else { + int stringLength = checksum.length(); + Property property = node.getProperty(JcrxName.JCRX_SUM); + List values = Arrays.asList(property.getValues()); + Integer indexToRemove = null; + values: for (int i = 0; i < values.size(); i++) { + Value value = values.get(i); + if (value.getString().length() == stringLength) { + indexToRemove = i; + break values; + } + } + if (indexToRemove != null) + values.set(indexToRemove, node.getSession().getValueFactory().createValue(checksum)); + else + values.add(0, node.getSession().getValueFactory().createValue(checksum)); + property.setValue(values.toArray(new Value[values.size()])); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot set checksum on " + node, e); + } + } + + /** Replace all checksums. */ + public static void setChecksums(Node node, List checksums) { + try { + node.setProperty(JcrxName.JCRX_SUM, checksums.toArray(new String[checksums.size()])); + } catch (RepositoryException e) { + throw new JcrException("Cannot set checksums on " + node, e); + } + } + + /** Replace all checksums. */ + public static List getChecksums(Node node) { + try { + List res = new ArrayList<>(); + if (!node.hasProperty(JcrxName.JCRX_SUM)) + return res; + Property property = node.getProperty(JcrxName.JCRX_SUM); + for (Value value : property.getValues()) { + res.add(value.getString()); + } + return res; + } catch (RepositoryException e) { + throw new JcrException("Cannot get checksums from " + node, e); + } + } + +// /** Replace all checksums with this single one. */ +// public static void setChecksum(Node node, String checksum) { +// setChecksums(node, Collections.singletonList(checksum)); +// } + + /** Retrieves the checksum with this algorithm, or null if not found. */ + public static String getChecksum(Node node, String algorithm) { + int stringLength; + switch (algorithm) { + case MD5: + stringLength = LENGTH_MD5; + break; + case SHA1: + stringLength = LENGTH_SHA1; + break; + case SHA256: + stringLength = LENGTH_SHA256; + break; + case SHA512: + stringLength = LENGTH_SHA512; + break; + default: + throw new IllegalArgumentException("Unkown algorithm " + algorithm); + } + return getChecksum(node, stringLength); + } + + /** Retrieves the checksum with this string length, or null if not found. */ + public static String getChecksum(Node node, int stringLength) { + try { + if (!node.hasProperty(JcrxName.JCRX_SUM)) + return null; + Property property = node.getProperty(JcrxName.JCRX_SUM); + for (Value value : property.getValues()) { + String str = value.getString(); + if (str.length() == stringLength) + return str; + } + return null; + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get checksum for " + node, e); + } + } + +} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxName.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxName.java new file mode 100644 index 000000000..9dd43adce --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/JcrxName.java @@ -0,0 +1,7 @@ +package org.argeo.jcr; + +/** Names declared by the JCR extensions. */ +public interface JcrxName { + /** The multiple property holding various coherent checksums. */ + public final static String JCRX_SUM = "{http://www.argeo.org/ns/jcrx}sum"; +} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java index 984a739aa..0cbad3341 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java +++ b/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java @@ -11,4 +11,7 @@ public interface JcrxType { /** Node type for the node containing the text. */ public final static String JCRX_XMLTEXT = "{http://www.argeo.org/ns/jcrx}xmltext"; + /** Mixin node type for a set of checksums. */ + public final static String JCRX_CSUM = "{http://www.argeo.org/ns/jcrx}csum"; + } diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxUtils.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxUtils.java deleted file mode 100644 index d5f8f18d9..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrxUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.argeo.jcr; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; - -/** Uilities around the JCR extensions. */ -public class JcrxUtils { - - /* - * XML - */ - /** - * Set as a subnode which will be exported as an XML element. - */ - public static String getXmlValue(Node node, String name) { - try { - if (!node.hasNode(name)) - throw new IllegalArgumentException("No XML text named " + name); - return node.getNode(name).getNode(Jcr.JCR_XMLTEXT).getProperty(Jcr.JCR_XMLCHARACTERS).getString(); - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot get " + name + " as XML text", e); - } - } - - /** - * Set as a subnode which will be exported as an XML element. - */ - public static void setXmlValue(Node node, String name, String value) { - try { - if (node.hasNode(name)) - node.getNode(name).getNode(Jcr.JCR_XMLTEXT).setProperty(Jcr.JCR_XMLCHARACTERS, value); - else - node.addNode(name, JcrxType.JCRX_XMLVALUE).addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT) - .setProperty(Jcr.JCR_XMLCHARACTERS, value); - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot set " + name + " as XML text", e); - } - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/QualifiedName.java b/org.argeo.jcr/src/org/argeo/jcr/QualifiedName.java deleted file mode 100644 index 419f74e1c..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/QualifiedName.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.argeo.jcr; - -/** Can be applied to {@link Enum}s in order to generate prefixed names. */ -public interface QualifiedName { - String name(); - - default String getPrefix() { - return null; - } - - default String getNamespace() { - return null; - } - - default String qualified() { - String prefix = getPrefix(); - return prefix != null ? prefix + ":" + name() : name(); - } - - default String withNamespace() { - String namespace = getNamespace(); - if (namespace == null) - throw new UnsupportedOperationException("No namespace is specified for " + getClass()); - return "{" + namespace + "}" + name(); - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd b/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd index c8c44ad4a..3eb0e7a3d 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd +++ b/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd @@ -9,3 +9,8 @@ [jcrx:xmltext] - jcr:xmlcharacters (STRING) mandatory + +[jcrx:csum] +mixin + - jcrx:sum (STRING) * + \ No newline at end of file diff --git a/org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java b/org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java index f0e8605c4..0984276dd 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java +++ b/org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java @@ -131,7 +131,7 @@ public abstract class AbstractUrlProxy implements ResourceProxy { } binary = session.getValueFactory().createBinary(in); content.setProperty(Property.JCR_DATA, binary); - JcrUtils.updateLastModifiedAndParents(node, null); + JcrUtils.updateLastModifiedAndParents(node, null, true); return node; } finally { JcrUtils.closeQuietly(binary);