blob: 2fe32ebca282b848b8c517c5ee1611ea80068398 [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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JcrPropertyMapCacheEntry {
/** Global logger */
private static 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 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 Object value, final 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(int i=0; i<values.length; i++) {
failIfCannotStore(values[i], node);
}
} else {
failIfCannotStore(value, node);
}
}
private void failIfCannotStore(final Object value, final Node node)
throws RepositoryException {
if (value instanceof InputStream) {
// InputStream is storable and calling createValue for nothing
// eats its contents
return;
}
final Value val = this.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 Value createValue(final Object obj, final 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 Object[] convertToObjectArray(final 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 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 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 <T> T convertToType(final Class<T> type,
final Node node,
final 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 {
final Object sourceObject = this.getPropertyValue();
if (targetIsArray) {
result = (T) convertToArray(new Object[] {sourceObject}, type.getComponentType(), node, dynamicClassLoader);
} else {
result = convertToType(-1, sourceObject, type, node, dynamicClassLoader);
}
}
} catch (final NumberFormatException vfe) {
LOGGER.info("converToType: Cannot convert value of " + this.getPropertyValueOrNull()
+ " to " + type, vfe);
} catch (final IllegalArgumentException vfe) {
LOGGER.info("converToType: Cannot convert value of " + this.getPropertyValueOrNull()
+ " to " + type, vfe);
} catch (final ValueFormatException vfe) {
LOGGER.info("converToType: Cannot convert value of " + this.getPropertyValueOrNull()
+ " to " + type, vfe);
} catch (RepositoryException re) {
LOGGER.info("converToType: Cannot get value of " + this.getPropertyValueOrNull(), re);
}
// fall back to nothing
return result;
}
private <T> T[] convertToArray(final Object[] sourceArray,
final Class<T> type,
final Node node,
final 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 <T> T convertToType(final int index,
final Object initialValue,
final Class<T> type,
final Node node,
final 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) ) {
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(), "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 cnfe) {
// ignore and use fallback
} catch (final IOException ioe) {
// 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) this.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 Converter getConverter(final 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 InputStream in, final 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);
}
}
}