/* * 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.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.lang.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<T>(); 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 (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); } // 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); } } }