org.apache.jackrabbit.vault.util.DocViewProperty2 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aem-sdk-api Show documentation
Show all versions of aem-sdk-api Show documentation
The Adobe Experience Manager SDK
/*
* 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