All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jackrabbit.vault.util.DocViewProperty2 Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jackrabbit.vault.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.jcr.Binary;
import javax.jcr.InvalidSerializedDataException;
import javax.jcr.NamespaceException;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.VersionException;

import org.apache.jackrabbit.api.ReferenceBinary;
import org.apache.jackrabbit.commons.jackrabbit.SimpleReferenceBinary;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.util.XMLChar;
import org.apache.jackrabbit.value.ValueFactoryImpl;
import org.apache.jackrabbit.value.ValueHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Immutable helper class that represents a JCR property in the FileVault (enhanced) document view format.
 * It contains formatting and parsing methods for serializing/deserializing enhanced
 * docview properties.
 * 
* The string representation adheres to the following grammar: *
 * prop:= [ "{" type "}" ] ( value | "[" [ value { "," value } ] "]" )
 * type := {@link PropertyType#nameFromValue(int)} | {@link #BINARY_REF}
 * value := is a string representation of the value where the following characters are escaped: ',\[{' with a leading '\'
 * 
 * 
* @see FileVault Document View Format * @since 3.6.0 */ public class DocViewProperty2 { public static final String BINARY_REF = "BinaryRef"; /** * name of the property */ private final Name name; /** * value(s) of the property. always contains at least one value if this is * not a mv property. */ private final List values; /** * indicates a multi-value property */ private final boolean isMultiValue; /** * type of this property (can be undefined) */ private final int type; /** * indicates a binary ref property */ private final boolean isReferenceProperty; /** * set of unambiguous property names (which never need an explicit type descriptor as the types are defined by the spec) */ private static final Set UNAMBIGUOUS = new HashSet<>(); static { UNAMBIGUOUS.add(NameConstants.JCR_PRIMARYTYPE); UNAMBIGUOUS.add(NameConstants.JCR_MIXINTYPES); } /** * Creates a new property based on an array of {@link Value}s * @param name the name of the property * @param values the values (always an array, may be empty), must not contain {@code null} items * @param type the type of the property * @param isMulti {@code true} in case this is a multivalue property * @param sort {@code true} in case the value array should be sorted first * @param useBinaryReferences to use the binary reference as value (if available) * @return the new property * @throws RepositoryException */ public static @NotNull DocViewProperty2 fromValues(@NotNull Name name, @NotNull Value[] values, int type, boolean isMulti, boolean sort, boolean useBinaryReferences) throws RepositoryException { List strValues = new ArrayList<>(); if (isMulti && sort) { Arrays.sort(values, ValueComparator.getInstance()); } for (Value value : values) { strValues.add(serializeValue(value, useBinaryReferences)); } Boolean isBinaryRef = null; if (type == PropertyType.BINARY) { // either only binary references or regular binaries for (String strValue : strValues) { boolean isCurrentValueBinaryRef = !strValue.isEmpty(); if (isBinaryRef == null) { isBinaryRef = isCurrentValueBinaryRef; } else { if (isBinaryRef != isCurrentValueBinaryRef) { throw new ValueFormatException("Mixed binary references and regular binary values in the same multi-value property is not supported"); } } } } if (isBinaryRef == null) { isBinaryRef = false; } return new DocViewProperty2(name, strValues, isMulti, type, isBinaryRef); } /** * Creates a new property based on a JCR {@link Property} object. * @param prop the JCR property * @param sort if {@code true} multi-value properties should be sorted * @param useBinaryReferences {@code true} to use binary references * @return the new property * @throws IllegalArgumentException if single value property and not exactly 1 value is given. * @throws RepositoryException if another error occurs */ public static @NotNull DocViewProperty2 fromProperty(@NotNull Property prop, boolean sort, boolean useBinaryReferences) throws RepositoryException { boolean isMultiValue = prop.getDefinition().isMultiple(); final Value[] values; if (isMultiValue) { values = prop.getValues(); } else { values = new Value[] { prop.getValue() }; } NameResolver nameResolver = new DefaultNamePathResolver(prop.getSession()); return fromValues(nameResolver.getQName(prop.getName()), values, prop.getType(), isMultiValue, sort, useBinaryReferences); } static String serializeValue(Value value, boolean useBinaryReferences) throws RepositoryException { // special handling for binaries String strValue = null; if (value.getType() == PropertyType.BINARY) { if (useBinaryReferences) { Binary bin = value.getBinary(); if (bin instanceof ReferenceBinary) { strValue = ((ReferenceBinary) bin).getReference(); } } if (strValue == null) { // leave value empty for non reference binaries or where reference is null strValue = ""; } } else { strValue = ValueHelper.serialize(value, false); } return strValue; } /** * Creates a new single-value property. * @param name name of the property * @param value value * @param type type of the property */ public DocViewProperty2(@NotNull Name name, @NotNull String value, int type) { this(name, Collections.singletonList(value), false, type, false); } /** * Creates a new single-value property with an undefined type. * @param name name of the property * @param value value */ public DocViewProperty2(@NotNull Name name, @NotNull String value) { this(name, Collections.singletonList(value), false, PropertyType.UNDEFINED, false); } /** * Creates a new multi-value property. * @param name name of the property * @param values values * @param type type of the property */ public DocViewProperty2(@NotNull Name name, @NotNull List values, int type) { this(name, values, true, type, false); } /** * Creates a new multi-value property with an undefined type. * @param name name of the property * @param values values */ public DocViewProperty2(@NotNull Name name, @NotNull List values) { this(name, values, true, PropertyType.UNDEFINED, false); } /** * Creates a new property. * @param name name of the property * @param values string representation of values * @param isMultiValue indicates if this is a multi-value property * @param type type of the property * @param isRef {@code true} to indicate that this is a binary reference property * @throws IllegalArgumentException if single value property and not exactly 1 value is given */ protected DocViewProperty2(@NotNull Name name, @NotNull List values, boolean isMultiValue, int type, boolean isRef) { this.name = name; this.values = Collections.unmodifiableList(values); this.isMultiValue = isMultiValue; // validate type if (type == PropertyType.UNDEFINED) { if (NameConstants.JCR_PRIMARYTYPE.equals(name) || NameConstants.JCR_MIXINTYPES.equals(name)) { type = PropertyType.NAME; } } this.type = type; if (!isMultiValue && values.size() != 1) { throw new IllegalArgumentException("Single value property needs exactly 1 value."); } this.isReferenceProperty = isRef; } /** * Parses a enhanced docview property string and returns the property. * @param name name of the property (either in qualified or extended form) * @param value (attribute) value * @throws IllegalArgumentException in case the given value does not follow the doc view property grammar * @return a property * @throws NamespaceException * @throws IllegalNameException */ public static @NotNull DocViewProperty2 parse(String name, String value, NameResolver nameResolver) throws IllegalNameException, NamespaceException { return parse(nameResolver.getQName(name), value); } /** * Parses a enhanced docview property string and returns the property. * @param name name of the property * @param value (attribute) value * @throws IllegalArgumentException in case the given value does not follow the doc view property grammar * @return a property * @throws NamespaceException * @throws IllegalNameException */ public static @NotNull DocViewProperty2 parse(Name name, String value) throws IllegalNameException, NamespaceException { boolean isMulti = false; boolean isBinaryRef = false; int type = PropertyType.UNDEFINED; int pos = 0; char state = 'b'; List vals = null; StringBuilder tmp = new StringBuilder(); int unicode = 0; int unicodePos = 0; while (pos < value.length()) { char c = value.charAt(pos++); switch (state) { case 'b': // begin (type or array or value) if (c == '{') { state = 't'; } else if (c == '[') { isMulti = true; state = 'v'; } else if (c == '\\') { state = 'e'; } else { tmp.append(c); state = 'v'; } break; case 'a': // array (array or value) if (c == '[') { isMulti = true; state = 'v'; } else if (c == '\\') { state = 'e'; } else { tmp.append(c); state = 'v'; } break; case 't': if (c == '}') { if (BINARY_REF.equals(tmp.toString())) { type = PropertyType.BINARY; isBinaryRef = true; } else { type = PropertyType.valueFromName(tmp.toString()); } tmp.setLength(0); state = 'a'; } else { tmp.append(c); } break; case 'v': // value if (c == '\\') { state = 'e'; } else if (c == ',' && isMulti) { if (vals == null) { vals = new LinkedList<>(); } vals.add(tmp.toString()); tmp.setLength(0); } else if (c == ']' && isMulti && pos == value.length()) { if (tmp.length() > 0 || vals != null) { if (vals == null) { vals = new LinkedList<>(); } vals.add(tmp.toString()); tmp.setLength(0); } } else { tmp.append(c); } break; case 'e': // escaped if (c == 'u') { state = 'u'; unicode = 0; unicodePos = 0; } else if (c == '0') { // special case to treat empty values. see JCR-3661 state = 'v'; if (vals == null) { vals = new LinkedList<>(); } } else { state = 'v'; tmp.append(c); } break; case 'u': // unicode escaped unicode = (unicode << 4) + Character.digit(c, 16); if (++unicodePos == 4) { tmp.appendCodePoint(unicode); state = 'v'; } break; } } if (isMulti) { // add value if missing ']' if (tmp.length() > 0) { if (vals == null) { vals = new LinkedList<>(); } vals.add(tmp.toString()); } if (vals == null) { vals = Collections.emptyList(); } } else { vals = Collections.singletonList(tmp.toString()); } return new DocViewProperty2(name, vals, isMulti, type, isBinaryRef); } /** * Formats (serializes) the given JCR property value according to the enhanced docview syntax. * @param prop the JCR property * @return the formatted string of the property value * @throws RepositoryException if a repository error occurs */ public static @NotNull String format(@NotNull Property prop) throws RepositoryException { return format(prop, false, false); } /** * Formats (serializes) the given JCR property value to the enhanced docview syntax. * @param prop the JCR property * @param sort if {@code true} multi-value properties are sorted * @param useBinaryReferences {@code true} to use binary references * @return the formatted string of the property value * @throws RepositoryException if a repository error occurs */ public static @NotNull String format(@NotNull Property prop, boolean sort, boolean useBinaryReferences) throws RepositoryException { return fromProperty(prop, sort, useBinaryReferences).formatValue(); } /** * Generates string representation of this DocView property value. * @return the string representation of the value */ public @NotNull String formatValue() { StringBuilder attrValue = new StringBuilder(); if (isAmbiguous(type, name)) { final String strType; if (isReferenceProperty) { strType = BINARY_REF; } else { strType = PropertyType.nameFromValue(type); } attrValue.append('{').append(strType).append('}'); } if (isMultiValue) { attrValue.append('['); } for (int i=0;i 0) { attrValue.append(','); } switch (type) { case PropertyType.STRING: case PropertyType.NAME: case PropertyType.PATH: case PropertyType.UNDEFINED: attrValue.append(escape(value, isMultiValue)); break; default: attrValue.append(value); } } } if (isMultiValue) { attrValue.append(']'); } return attrValue.toString(); } /** * Escapes the value * @param buf buffer to append to * @param value value to escape * @param isMultiValue indicates multi-value property * @deprecated Rather use {@link #escape(String, boolean)} */ @Deprecated protected static void escape(StringBuffer buf, String value, boolean isMultiValue) { buf.append(escape(value, isMultiValue)); } /** * Escapes the value * @param value value to escape * @param isMultiValue indicates multi-value property * @return the escaped value */ protected static String escape(String value, boolean isMultiValue) { StringBuilder buf = new StringBuilder(); for (int i=0; i> 12) & 15]); buf.append(Text.hexTable[(c >> 8) & 15]); buf.append(Text.hexTable[(c >> 4) & 15]); buf.append(Text.hexTable[c & 15]); } else { buf.append(c); } } return buf.toString(); } /** * Checks if the type of the given property is ambiguous in respect to it's * property definition. The current implementation just returns {@code false} for * some well known property names and for type = PropertyType.STRING or PropertyType.UNDEFINED. * * @param type the type * @param name the name * @return {@code true} if type information should be emitted, otherwise {@code false} */ private static boolean isAmbiguous(int type, Name name) { return type != PropertyType.STRING && type != PropertyType.UNDEFINED && !UNAMBIGUOUS.contains(name); } /** * Returns a suitable qualified JCR name. */ private String getQualifiedName(Session session, Name name) throws RepositoryException { // TODO: could use nsresolver instead String nsuri = name.getNamespaceURI(); if (nsuri.isEmpty()) { return name.getLocalName(); } else { return session.getNamespacePrefix(nsuri) + ":" + name.getLocalName(); } } /** * Sets this property on the given node. * * @param node the node * @return {@code true} if the value was modified. * @throws RepositoryException if a repository error occurs */ public boolean apply(@NotNull Node node) throws RepositoryException { String qualifiedName = getQualifiedName(node.getSession(), name); Property prop = node.hasProperty(qualifiedName) ? node.getProperty(qualifiedName) : null; // check if multiple flags are equal if (prop != null && isMultiValue != prop.getDefinition().isMultiple()) { prop.remove(); prop = null; } if (prop != null) { int propType = prop.getType(); if (propType != type && (propType != PropertyType.STRING || type != PropertyType.UNDEFINED)) { // never compare if types differ prop = null; } } if (isMultiValue) { Value[] vs = prop == null ? null : prop.getValues(); if (type == PropertyType.BINARY) { return applyBinary(node, vs); } if (vs != null && vs.length == values.size()) { // quick check all values boolean modified = false; for (int i=0; i binaryValues = new ArrayList<>(values.size()); if (!isReferenceProperty) { for (String value : values) { // empty string is used for binary properties which should not be touched! if (!value.isEmpty()) { throw new InvalidSerializedDataException("Inline binaries are only supported as binary references, but is " + value); } } // just silently ignore binaries with only empty string values return false; } try { boolean modified = false; for (int n=0; n < values.size(); n++) { String value = values.get(n); ReferenceBinary ref = new SimpleReferenceBinary(value); Value binaryValue = node.getSession().getValueFactory().createValue(ref); binaryValues.add(binaryValue); // compare with existing value if (!modified && existingValues != null && n < existingValues.length && existingValues[n] != null) { Binary existingBinary = existingValues[0].getBinary(); if (!existingBinary.equals(binaryValue.getBinary())) { modified = true; } } else { modified = true; } } if (!modified) { return false; } String qualifiedName = getQualifiedName(node.getSession(), name); if (isMultiValue) { node.setProperty(qualifiedName, binaryValues.toArray(new Value[0])); } else { node.setProperty(qualifiedName, binaryValues.get(0)); } // the binary property is always modified (TODO: check if still correct with JCRVLT-110) return true; } finally { for (Value value : binaryValues) { value.getBinary().dispose(); } } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (isMultiValue ? 1231 : 1237); result = prime * result + (isReferenceProperty ? 1231 : 1237); result = prime * result + name.hashCode(); result = prime * result + type; result = prime * result + values.hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; DocViewProperty2 other = (DocViewProperty2) obj; if (isMultiValue != other.isMultiValue) return false; if (isReferenceProperty != other.isReferenceProperty) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (type != other.type) return false; return (values.equals(other.values)); } /** * This does not return the string representation of the enhanced docview property value but rather a descriptive string including the property name for debugging purposes. * Use {@link #formatValue()}, {@link #format(Property)} or {@link #format(Property, boolean, boolean)} to get the enhanced docview string representation of the value. */ @Override public String toString() { return "DocViewProperty2 [name=" + name + ", values=" + String.join(",", values) + ", isMultiValue=" + isMultiValue + ", type=" + PropertyType.nameFromValue(type) + ", isReferenceProperty=" + isReferenceProperty + "]"; } public @NotNull Name getName() { return name; } public boolean isMultiValue() { return isMultiValue; } public boolean isReferenceProperty() { return isReferenceProperty; } /** * * @return one of the values defined in {@link PropertyType} */ public int getType() { return type; } private int getSafeType() { return type == PropertyType.UNDEFINED ? PropertyType.STRING : type; } public @NotNull Optional getStringValue() { if (!values.isEmpty()) { return Optional.of(values.get(0)); } return Optional.empty(); } public @NotNull List getStringValues() { return values; } /** * @param valueFactory the value factory to use for converting the underlying string to the JCR value * @return the value or empty if no value set. For multi value only the first item is returned * @throws ValueFormatException * @since 3.7.0 */ public @NotNull Optional getValue(@NotNull ValueFactory valueFactory) throws ValueFormatException { if (!values.isEmpty()) { return Optional.of(valueFactory.createValue(values.get(0), getSafeType())); } return Optional.empty(); } /** * @param valueFactory the value factory to use for converting the underlying string to the JCR value * @return the list of values, may be empty. In case of single value entry just a single value list. * @throws ValueFormatException * @since 3.7.0 */ public @NotNull List getValues(@NotNull ValueFactory valueFactory) throws ValueFormatException { try { return values.stream().map(v -> { try { return valueFactory.createValue(v, getSafeType()); } catch (ValueFormatException e) { throw new UncheckedValueFormatException(e); } }).collect(Collectors.toList()); } catch (UncheckedValueFormatException e) { throw e.getCause(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy