| /* |
| * 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; |
| |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.jcr.Node; |
| import javax.jcr.Property; |
| import javax.jcr.PropertyIterator; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Value; |
| |
| import org.apache.jackrabbit.util.ISO9075; |
| import org.apache.jackrabbit.util.Text; |
| import org.apache.sling.api.resource.ModifiableValueMap; |
| import org.apache.sling.api.resource.ValueMap; |
| import org.apache.sling.jcr.resource.internal.helper.JcrPropertyMapCacheEntry; |
| |
| /** |
| * This implementation of the value map allows to change |
| * the properties. |
| */ |
| public class JcrValueMap implements ValueMap { |
| |
| /** The underlying node. */ |
| protected final Node node; |
| |
| /** A cache for the properties. */ |
| protected final Map<String, JcrPropertyMapCacheEntry> cache = new LinkedHashMap<>(); |
| |
| /** A cache for the values. */ |
| protected final Map<String, Object> valueCache = new LinkedHashMap<>(); |
| |
| /** Has the node been read completely? */ |
| private boolean fullyRead = false; |
| |
| /** Helper data object */ |
| protected final HelperData helper; |
| |
| /** |
| * Constructor |
| * @param node The underlying node. |
| * @param helper Helper data object |
| */ |
| public JcrValueMap(final Node node, final HelperData helper) { |
| this.node = node; |
| this.helper = helper; |
| } |
| |
| // ---------- ValueMap |
| |
| protected String checkKey(final String key) { |
| if (key == null) { |
| throw new NullPointerException("Key must not be null."); |
| } |
| if (key.startsWith("./")) { |
| return key.substring(2); |
| } |
| return key; |
| } |
| |
| /** |
| * @see org.apache.sling.api.resource.ValueMap#get(java.lang.String, java.lang.Class) |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| public <T> T get(final String aKey, final Class<T> type) { |
| final String key = checkKey(aKey); |
| if (type == null) { |
| return (T) get(key); |
| } |
| |
| final JcrPropertyMapCacheEntry entry = this.read(key); |
| if (entry == null) { |
| return null; |
| } |
| return entry.convertToType(type, node, helper.getDynamicClassLoader()); |
| } |
| |
| /** |
| * @see org.apache.sling.api.resource.ValueMap#get(java.lang.String, java.lang.Object) |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| public <T> T get(final String aKey, final T defaultValue) { |
| final String key = checkKey(aKey); |
| if (defaultValue == null) { |
| return (T) get(key); |
| } |
| |
| // special handling in case the default value implements one |
| // of the interface types supported by the convertToType method |
| Class<T> type = (Class<T>) normalizeClass(defaultValue.getClass()); |
| |
| T value = get(key, type); |
| if (value == null) { |
| value = defaultValue; |
| } |
| |
| return value; |
| } |
| |
| // ---------- Map |
| |
| /** |
| * @see java.util.Map#get(java.lang.Object) |
| */ |
| @Override |
| public Object get(final Object aKey) { |
| final String key = checkKey(aKey.toString()); |
| final JcrPropertyMapCacheEntry entry = this.read(key); |
| final Object value = (entry == null ? null : entry.getPropertyValueOrNull()); |
| return value; |
| } |
| |
| /** |
| * @see java.util.Map#containsKey(java.lang.Object) |
| */ |
| @Override |
| public boolean containsKey(final Object key) { |
| return get(key) != null; |
| } |
| |
| /** |
| * @see java.util.Map#containsValue(java.lang.Object) |
| */ |
| @Override |
| public boolean containsValue(final Object value) { |
| readFully(); |
| return valueCache.containsValue(value); |
| } |
| |
| /** |
| * @see java.util.Map#isEmpty() |
| */ |
| @Override |
| public boolean isEmpty() { |
| return size() == 0; |
| } |
| |
| /** |
| * @see java.util.Map#size() |
| */ |
| @Override |
| public int size() { |
| readFully(); |
| return cache.size(); |
| } |
| |
| /** |
| * @see java.util.Map#entrySet() |
| */ |
| @Override |
| public Set<java.util.Map.Entry<String, Object>> entrySet() { |
| readFully(); |
| final Map<String, Object> sourceMap; |
| if (cache.size() == valueCache.size()) { |
| sourceMap = valueCache; |
| } else { |
| sourceMap = transformEntries(cache); |
| } |
| return Collections.unmodifiableSet(sourceMap.entrySet()); |
| } |
| |
| /** |
| * @see java.util.Map#keySet() |
| */ |
| @Override |
| public Set<String> keySet() { |
| readFully(); |
| return Collections.unmodifiableSet(cache.keySet()); |
| } |
| |
| /** |
| * @see java.util.Map#values() |
| */ |
| @Override |
| public Collection<Object> values() { |
| readFully(); |
| final Map<String, Object> sourceMap; |
| if (cache.size() == valueCache.size()) { |
| sourceMap = valueCache; |
| } else { |
| sourceMap = transformEntries(cache); |
| } |
| return Collections.unmodifiableCollection(sourceMap.values()); |
| } |
| |
| /** |
| * Return the path of the current node. |
| * |
| * @return the path |
| * @throws IllegalStateException If a repository exception occurs |
| */ |
| public String getPath() { |
| try { |
| return node.getPath(); |
| } catch (final RepositoryException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| // ---------- Helpers to access the node's property ------------------------ |
| |
| /** |
| * Put a single property into the cache |
| * @param prop The property to be cached |
| * @return A JcrPropertyMapCacheEntry for the given property |
| * @throws IllegalArgumentException if a repository exception occurs |
| */ |
| private JcrPropertyMapCacheEntry cacheProperty(final Property prop) { |
| try { |
| // calculate the key |
| final String name = prop.getName(); |
| String key = null; |
| if (name.indexOf("_x") != -1) { |
| // for compatibility with older versions we use the (wrong) |
| // ISO9075 path encoding |
| key = ISO9075.decode(name); |
| if (key.equals(name)) { |
| key = null; |
| } |
| } |
| if (key == null) { |
| key = Text.unescapeIllegalJcrChars(name); |
| } |
| JcrPropertyMapCacheEntry entry = cache.get(key); |
| if (entry == null) { |
| entry = new JcrPropertyMapCacheEntry(prop); |
| cache.put(key, entry); |
| |
| final Object defaultValue = entry.getPropertyValue(); |
| if (defaultValue != null) { |
| valueCache.put(key, entry.getPropertyValue()); |
| } |
| } |
| return entry; |
| } catch (final RepositoryException re) { |
| throw new IllegalArgumentException(re); |
| } |
| } |
| |
| /** |
| * Read a single property. |
| * @throws IllegalArgumentException if a repository exception occurs |
| */ |
| JcrPropertyMapCacheEntry read(final String name) { |
| // check for empty key |
| if (name.length() == 0) { |
| return null; |
| } |
| // if the name is a path, we should handle this differently |
| if (name.indexOf('/') != -1) { |
| // first a compatibility check with the old (wrong) ISO9075 |
| // encoding |
| final String path = ISO9075.encodePath(name); |
| try { |
| if (node.hasProperty(path)) { |
| return new JcrPropertyMapCacheEntry(node.getProperty(path)); |
| } |
| } catch (final RepositoryException re) { |
| throw new IllegalArgumentException(re); |
| } |
| // now we do a proper segment by segment encoding |
| final StringBuilder sb = new StringBuilder(); |
| int pos = 0; |
| int lastPos = -1; |
| while (pos < name.length()) { |
| if (name.charAt(pos) == '/') { |
| if (lastPos + 1 < pos) { |
| sb.append(Text.escapeIllegalJcrChars(name.substring(lastPos + 1, pos))); |
| } |
| sb.append('/'); |
| lastPos = pos; |
| } |
| pos++; |
| } |
| if (lastPos + 1 < pos) { |
| sb.append(Text.escapeIllegalJcrChars(name.substring(lastPos + 1))); |
| } |
| final String newPath = sb.toString(); |
| try { |
| if (node.hasProperty(newPath)) { |
| return new JcrPropertyMapCacheEntry(node.getProperty(newPath)); |
| } |
| } catch (final RepositoryException re) { |
| throw new IllegalArgumentException(re); |
| } |
| |
| return null; |
| } |
| |
| // check cache |
| JcrPropertyMapCacheEntry cachedValued = cache.get(name); |
| if (fullyRead || cachedValued != null) { |
| return cachedValued; |
| } |
| |
| try { |
| final String key = escapeKeyName(name); |
| if (node.hasProperty(key)) { |
| final Property prop = node.getProperty(key); |
| return cacheProperty(prop); |
| } |
| } catch (final RepositoryException re) { |
| throw new IllegalArgumentException(re); |
| } |
| |
| try { |
| // for compatibility with older versions we use the (wrong) ISO9075 path |
| // encoding |
| final String oldKey = ISO9075.encodePath(name); |
| if (node.hasProperty(oldKey)) { |
| final Property prop = node.getProperty(oldKey); |
| return cacheProperty(prop); |
| } |
| } catch (final RepositoryException re) { |
| // we ignore this |
| } |
| |
| // property not found |
| return null; |
| } |
| |
| /** |
| * Handles key name escaping by taking into consideration if it contains a |
| * registered prefix |
| * |
| * @param key the key to escape |
| * @return escaped key name |
| * @throws RepositoryException if the repository's namespace prefixes cannot be retrieved |
| */ |
| protected String escapeKeyName(final String key) throws RepositoryException { |
| final int indexOfPrefix = key.indexOf(':'); |
| // check if colon is neither the first nor the last character |
| if (indexOfPrefix > 0 && key.length() > indexOfPrefix + 1) { |
| final String prefix = key.substring(0, indexOfPrefix); |
| for (final String existingPrefix : this.helper.getNamespacePrefixes(this.node.getSession())) { |
| if (existingPrefix.equals(prefix)) { |
| return prefix |
| + ":" |
| + Text.escapeIllegalJcrChars(key |
| .substring(indexOfPrefix + 1)); |
| } |
| } |
| } |
| return Text.escapeIllegalJcrChars(key); |
| } |
| |
| /** |
| * Read all properties. |
| * @throws IllegalArgumentException if a repository exception occurs |
| */ |
| void readFully() { |
| if (!fullyRead) { |
| try { |
| final PropertyIterator pi = node.getProperties(); |
| while (pi.hasNext()) { |
| final Property prop = pi.nextProperty(); |
| this.cacheProperty(prop); |
| } |
| fullyRead = true; |
| } catch (final RepositoryException re) { |
| throw new IllegalArgumentException(re); |
| } |
| } |
| } |
| |
| // ---------- Implementation helper |
| |
| private Class<?> normalizeClass(Class<?> type) { |
| if (Calendar.class.isAssignableFrom(type)) { |
| type = Calendar.class; |
| } else if (Date.class.isAssignableFrom(type)) { |
| type = Date.class; |
| } else if (Value.class.isAssignableFrom(type)) { |
| type = Value.class; |
| } else if (Property.class.isAssignableFrom(type)) { |
| type = Property.class; |
| } |
| return type; |
| } |
| |
| private Map<String, Object> transformEntries(final Map<String, JcrPropertyMapCacheEntry> map) { |
| |
| final Map<String, Object> transformedEntries = new LinkedHashMap<>(map.size()); |
| for (final Map.Entry<String, JcrPropertyMapCacheEntry> entry : map.entrySet()) |
| transformedEntries.put(entry.getKey(), entry.getValue().getPropertyValueOrNull()); |
| |
| return transformedEntries; |
| } |
| |
| // ---------- Map |
| |
| /** |
| * @see java.util.Map#clear() |
| */ |
| @Override |
| public void clear() { |
| throw new UnsupportedOperationException("clear"); |
| } |
| |
| /** |
| * @see java.util.Map#put(java.lang.Object, java.lang.Object) |
| */ |
| @Override |
| public Object put(final String aKey, final Object value) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @see java.util.Map#putAll(java.util.Map) |
| */ |
| @Override |
| public void putAll(final Map<? extends String, ? extends Object> t) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @see java.util.Map#remove(java.lang.Object) |
| */ |
| @Override |
| public Object remove(final Object aKey) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public String toString() { |
| final StringBuilder sb = new StringBuilder(); |
| if (this instanceof ModifiableValueMap) { |
| sb.append("JcrModifiablePropertyMap"); |
| } else { |
| sb.append("JcrPropertyMap"); |
| } |
| sb.append(" [node="); |
| sb.append(this.node); |
| sb.append(", values={"); |
| final Iterator<Map.Entry<String, Object>> iter = this.entrySet().iterator(); |
| boolean first = true; |
| while (iter.hasNext()) { |
| if (first) { |
| first = false; |
| } else { |
| sb.append(", "); |
| } |
| final Map.Entry<String, Object> e = iter.next(); |
| sb.append(e.getKey()); |
| sb.append("="); |
| sb.append(e.getValue()); |
| } |
| sb.append("}]"); |
| return sb.toString(); |
| } |
| } |