blob: d5f1efd4f20d1351a1575575b38b070099fe64cb [file] [log] [blame]
/*
* 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.sling.jcr.resource.internal.helper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
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.ValueFormatException;
import org.apache.commons.lang3.ArrayUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JcrPropertyMapCacheEntry {
/** Global logger */
private static final Logger LOGGER = LoggerFactory.getLogger(JcrPropertyMapCacheEntry.class);
/** The JCR property - only set for existing values. */
private final Property property;
/** Whether this is an array or a single value. */
private final boolean isArray;
/** The value of the object. */
private final Object propertyValue;
/**
* Create a new cache entry from a property.
*
* @param prop the property
* @throws RepositoryException if the provided property cannot be converted to a Java Object
*/
public JcrPropertyMapCacheEntry(final @NotNull Property prop) throws RepositoryException {
this.property = prop;
this.isArray = prop.isMultiple();
if (property.getType() != PropertyType.BINARY) {
this.propertyValue = JcrResourceUtil.toJavaObject(prop);
} else {
this.propertyValue = null;
}
}
/**
* Create a new cache entry from a value.
* @param value the value
* @param node the node
* @throws RepositoryException if the provided value cannot be stored
*/
public JcrPropertyMapCacheEntry(final @NotNull Object value, final @NotNull Node node) throws RepositoryException {
this.property = null;
this.propertyValue = value;
this.isArray = value.getClass().isArray();
// check if values can be stored in JCR
if (isArray) {
final Object[] values = convertToObjectArray(value);
for (Object o : values) {
failIfCannotStore(o, node);
}
} else {
failIfCannotStore(value, node);
}
}
private static void failIfCannotStore(final @NotNull Object value, final @NotNull Node node) throws RepositoryException {
if (value instanceof InputStream) {
// InputStream is storable and calling createValue for nothing
// eats its contents
return;
}
final Value val = createValue(value, node);
if (val == null) {
throw new IllegalArgumentException("Value can't be stored in the repository: " + value);
}
}
/**
* Create a value for the object.
* If the value type is supported directly through a jcr property type,
* the corresponding value is created. If the value is serializable,
* it is serialized through an object stream. Otherwise null is returned.
*
* @param obj the object
* @param node the node
* @return the converted value
*/
private static @Nullable Value createValue(final @NotNull Object obj, final @NotNull Node node) throws RepositoryException {
final Session session = node.getSession();
Value value = JcrResourceUtil.createValue(obj, session);
if (value == null && obj instanceof Serializable) {
try {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
value = session.getValueFactory().createValue(session.getValueFactory().createBinary(bais));
} catch (IOException ioe) {
// we ignore this here and return null
}
}
return value;
}
/**
* Convert the object to an array
* @param value The array
* @return an object array
*/
private static @NotNull Object[] convertToObjectArray(final @NotNull Object value) {
final Object[] values;
if (value instanceof long[]) {
values = ArrayUtils.toObject((long[]) value);
} else if (value instanceof int[]) {
values = ArrayUtils.toObject((int[]) value);
} else if (value instanceof double[]) {
values = ArrayUtils.toObject((double[]) value);
} else if (value instanceof byte[]) {
values = ArrayUtils.toObject((byte[]) value);
} else if (value instanceof float[]) {
values = ArrayUtils.toObject((float[]) value);
} else if (value instanceof short[]) {
values = ArrayUtils.toObject((short[]) value);
} else if (value instanceof boolean[]) {
values = ArrayUtils.toObject((boolean[]) value);
} else if (value instanceof char[]) {
values = ArrayUtils.toObject((char[]) value);
} else {
values = (Object[]) value;
}
return values;
}
/**
* Whether this value is an array or not
* @return {@code true} if an array.
*/
public boolean isArray() {
return this.isArray;
}
/**
* Get the current property value.
* @return The current value
* @throws RepositoryException If something goes wrong
*/
public @NotNull Object getPropertyValue() throws RepositoryException {
return this.propertyValue != null ? this.propertyValue : JcrResourceUtil.toJavaObject(property);
}
/**
* Get the current property value.
* @return The current value or {@code null} if not possible.
*/
public @Nullable Object getPropertyValueOrNull() {
try {
return getPropertyValue();
} catch (final RepositoryException e) {
return null;
}
}
/**
* Convert the default value to the given type
* @param type The type class
* @param node The node
* @param dynamicClassLoader The classloader
* @param <T> The type
* @return The converted object
*/
@SuppressWarnings("unchecked")
public @Nullable<T> T convertToType(final @NotNull Class<T> type,
final @NotNull Node node,
final @Nullable ClassLoader dynamicClassLoader) {
T result = null;
try {
final boolean targetIsArray = type.isArray();
if (this.isArray) {
final Object[] sourceArray = convertToObjectArray(this.getPropertyValue());
if (targetIsArray) {
result = (T) convertToArray(sourceArray, type.getComponentType(), node, dynamicClassLoader);
} else if (sourceArray.length > 0) {
result = convertToType(-1, sourceArray[0], type, node, dynamicClassLoader);
}
} else {
// source is not multivalued
final Object sourceObject = this.getPropertyValue();
if (targetIsArray) {
result = (T) convertToArray(sourceObject, type.getComponentType(), node, dynamicClassLoader);
} else {
result = convertToType(-1, sourceObject, type, node, dynamicClassLoader);
}
}
} catch (final IllegalArgumentException | ValueFormatException vfe) {
LOGGER.info("convertToType: Cannot convert value of {} to {}.", this.getPropertyValueOrNull(), type, vfe);
} catch (RepositoryException re) {
LOGGER.info("convertToType: Cannot get value of {}", this.getPropertyValueOrNull(), re);
}
// fall back to nothing
return result;
}
private @NotNull<T> T[] convertToArray(final @NotNull Object source,
final @NotNull Class<T> type,
final @NotNull Node node,
final @Nullable ClassLoader dynamicClassLoader) throws RepositoryException {
List<T> values = new ArrayList<>();
T value = convertToType(-1, source, type, node, dynamicClassLoader);
if (value != null) {
values.add(value);
}
@SuppressWarnings("unchecked")
T[] result = (T[]) Array.newInstance(type, values.size());
return values.toArray(result);
}
private @NotNull<T> T[] convertToArray(final @NotNull Object[] sourceArray,
final @NotNull Class<T> type,
final @NotNull Node node,
final @Nullable ClassLoader dynamicClassLoader) throws RepositoryException {
List<T> values = new ArrayList<>();
for (int i = 0; i < sourceArray.length; i++) {
T value = convertToType(i, sourceArray[i], type, node, dynamicClassLoader);
if (value != null) {
values.add(value);
}
}
@SuppressWarnings("unchecked")
T[] result = (T[]) Array.newInstance(type, values.size());
return values.toArray(result);
}
@SuppressWarnings("unchecked")
private @Nullable<T> T convertToType(final int index,
final @NotNull Object initialValue,
final @NotNull Class<T> type,
final @NotNull Node node,
final @Nullable ClassLoader dynamicClassLoader) throws RepositoryException {
if (type.isInstance(initialValue)) {
return (T) initialValue;
}
Object value = initialValue;
// special case input stream first
if (value instanceof InputStream) {
// object input stream
if (ObjectInputStream.class.isAssignableFrom(type)) {
try {
return (T) new PropertyObjectInputStream((InputStream) value, dynamicClassLoader);
} catch (final IOException ioe) {
// ignore and use fallback
}
// any number: length of binary
} else if (Number.class.isAssignableFrom(type)) {
// avoid NPE if this instance has not been created from a property (see SLING-11465)
if (property == null) {
return null;
}
if (index == -1) {
value = Long.valueOf(this.property.getLength());
} else {
value = Long.valueOf(this.property.getLengths()[index]);
}
// string: read binary
} else if (String.class == type) {
final InputStream in = (InputStream) value;
try {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final byte[] buffer = new byte[2048];
int l;
while ((l = in.read(buffer)) >= 0) {
if (l > 0) {
baos.write(buffer, 0, l);
}
}
value = new String(baos.toByteArray(), StandardCharsets.UTF_8);
} catch (final IOException e) {
throw new IllegalArgumentException(e);
} finally {
try {
in.close();
} catch (final IOException ignore) {
// ignore
}
}
// any serializable
} else if (Serializable.class.isAssignableFrom(type)) {
ObjectInputStream ois = null;
try {
ois = new PropertyObjectInputStream((InputStream) value, dynamicClassLoader);
final Object obj = ois.readObject();
if (type.isInstance(obj)) {
return (T) obj;
}
value = obj;
} catch (final ClassNotFoundException | IOException cnfe) {
// ignore and use fallback
} finally {
if (ois != null) {
try {
ois.close();
} catch (final IOException ignore) {
// ignore
}
}
}
}
}
if (String.class == type) {
return (T) getConverter(value).toString();
} else if (Byte.class == type) {
return (T) getConverter(value).toByte();
} else if (Short.class == type) {
return (T) getConverter(value).toShort();
} else if (Integer.class == type) {
return (T) getConverter(value).toInteger();
} else if (Long.class == type) {
return (T) getConverter(value).toLong();
} else if (Float.class == type) {
return (T) getConverter(value).toFloat();
} else if (Double.class == type) {
return (T) getConverter(value).toDouble();
} else if (BigDecimal.class == type) {
return (T) getConverter(value).toBigDecimal();
} else if (Boolean.class == type) {
return (T) getConverter(value).toBoolean();
} else if (Date.class == type) {
return (T) getConverter(value).toDate();
} else if (Calendar.class == type) {
return (T) getConverter(value).toCalendar();
} else if (ZonedDateTime.class == type) {
Calendar calendar = getConverter(value).toCalendar();
return (T) ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId().normalized());
} else if (Value.class == type) {
return (T) createValue(value, node);
} else if (Property.class == type) {
return (T) this.property;
}
// fallback in case of unsupported type
return null;
}
/**
* Create a converter for an object.
*
* @param value The object to convert
* @return A converter for {@code value}
*/
private static @NotNull Converter getConverter(final @NotNull Object value) {
if (value instanceof Number) {
// byte, short, int, long, double, float, BigDecimal
return new NumberConverter((Number) value);
} else if (value instanceof Boolean) {
return new BooleanConverter((Boolean) value);
} else if (value instanceof Date) {
return new DateConverter((Date) value);
} else if (value instanceof Calendar) {
return new CalendarConverter((Calendar) value);
} else if (value instanceof ZonedDateTime) {
return new ZonedDateTimeConverter((ZonedDateTime) value);
}
// default string based
return new StringConverter(value);
}
/**
* This is an extended version of the object input stream which uses the
* thread context class loader.
*/
private static class PropertyObjectInputStream extends ObjectInputStream {
private final ClassLoader classloader;
public PropertyObjectInputStream(final @NotNull InputStream in, final @Nullable ClassLoader classLoader) throws IOException {
super(in);
this.classloader = classLoader;
}
/**
* @see java.io.ObjectInputStream#resolveClass(java.io.ObjectStreamClass)
*/
@Override
protected Class<?> resolveClass(final ObjectStreamClass classDesc)
throws IOException, ClassNotFoundException {
if (this.classloader != null) {
return this.classloader.loadClass(classDesc.getName());
}
return super.resolveClass(classDesc);
}
}
}