/** * This file Copyright (c) 2010-2012 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.jcr.util; import info.magnolia.link.LinkException; import info.magnolia.link.LinkTransformerManager; import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Map based representation of JCR content. This class is for instance used in template scripts to allow notations like * <code>content.propName</code>. It first tries to read a property with name (key) and if not present checks for the * presence of child node. Few special property names map to the JCR methods: \@name, \@id, \@path, \@level, \@nodeType * * @version $Id$ */ public class ContentMap implements Map<String, Object> { private final static Logger log = LoggerFactory.getLogger(ContentMap.class); private final Node content; /** * Represents getters of the node itself. */ private final Map<String, Method> specialProperties = new HashMap<String, Method>(); public ContentMap(Node content) { if (content == null) { throw new NullPointerException("ContentMap doesn't accept null content"); } this.content = content; // Supported special types are: @nodeType @name, @path @depth (and their deprecated forms - see // convertDeprecatedProps() for details) Class<? extends Node> clazz = content.getClass(); try { specialProperties.put("name", clazz.getMethod("getName", (Class<?>[]) null)); specialProperties.put("id", clazz.getMethod("getIdentifier", (Class<?>[]) null)); specialProperties.put("path", clazz.getMethod("getPath", (Class<?>[]) null)); specialProperties.put("depth", clazz.getMethod("getDepth", (Class<?>[]) null)); specialProperties.put("nodeType", clazz.getMethod("getPrimaryNodeType", (Class<?>[]) null)); } catch (SecurityException e) { log.debug( "Failed to gain access to Node get***() method. Check VM security settings. " + e.getLocalizedMessage(), e); } catch (NoSuchMethodException e) { log.debug( "Failed to retrieve get***() method of Node class. Check the classpath for conflicting version of JCR classes. " + e.getLocalizedMessage(), e); } } @Override public boolean containsKey(Object key) { String strKey = convertKey(key); if (!isValidKey(strKey)) { return false; } if (isSpecialProperty(strKey)) { return true; } try { return content.hasProperty(strKey); } catch (RepositoryException e) { // ignore, most likely invalid name } return false; } private String convertKey(Object key) { if (key == null) { return null; } try { return (String) key; } catch (ClassCastException e) { log.debug("Invalid key. Expected String, but got {}.", key.getClass().getName()); } return null; } private boolean isValidKey(String strKey) { return !StringUtils.isBlank(strKey); } private boolean isSpecialProperty(String strKey) { if (!strKey.startsWith("@")) { return false; } strKey = convertDeprecatedProps(strKey); return specialProperties.containsKey(StringUtils.removeStart(strKey, "@")); } /** * @return a property name - in case the one handed in is known to be deprecated it'll be converted, else the * original one is returned. */ private String convertDeprecatedProps(String strKey) { // in the past we allowed both lower and upper case notation ... if ("@UUID".equals(strKey) || "@uuid".equals(strKey)) { return "@id"; } else if ("@handle".equals(strKey)) { return "@path"; } else if ("@level".equals(strKey)) { return "@depth"; } return strKey; } @Override public Object get(Object key) { String keyStr; try { keyStr = (String) key; } catch (ClassCastException e) { throw new ClassCastException("ContentMap accepts only String as a parameters, provided object was of type " + (key == null ? "null" : key.getClass().getName())); } Object prop = getNodeProperty(keyStr); if (prop == null) { keyStr = convertDeprecatedProps(keyStr); return getSpecialProperty(keyStr); } return prop; } private Object getSpecialProperty(String strKey) { if (isSpecialProperty(strKey)) { final Method method = specialProperties.get(StringUtils.removeStart(strKey, "@")); try { return method.invoke(content, null); } catch (Exception e) { throw new RuntimeException(e); } } return null; } private Object getNodeProperty(String keyStr) { try { if (content.hasProperty(keyStr)) { Property prop = content.getProperty(keyStr); int type = prop.getType(); if (type == PropertyType.DATE) { return prop.getDate(); } else if (type == PropertyType.BINARY) { // this should actually never happen. there is no reason why anyone should stream binary data into // template ... or is there? } else if(type == PropertyType.BOOLEAN){ return prop.getBoolean(); } else if(type == PropertyType.LONG){ return prop.getLong(); } else if(type == PropertyType.DOUBLE){ return prop.getDouble(); } else if (prop.isMultiple()) { Value[] values = prop.getValues(); String[] valueStrings = new String[values.length]; for (int j = 0; j < values.length; j++) { try { valueStrings[j] = values[j].getString(); } catch (RepositoryException e) { log.debug(e.getMessage()); } } return valueStrings; } else { try { return info.magnolia.link.LinkUtil.convertLinksFromUUIDPattern(prop.getString(), LinkTransformerManager.getInstance().getBrowserLink(content.getPath())); } catch (LinkException e) { log.warn("Failed to parse links with from " + prop.getName(), e); } } // don't we want to honor other types (e.g. numbers? ) return prop.getString(); } // property doesn't exist, but maybe child of that name does if (content.hasNode(keyStr)) { return new ContentMap(content.getNode(keyStr)); } } catch (PathNotFoundException e) { // ignore, property doesn't exist } catch (RepositoryException e) { log.warn("Failed to retrieve {} on {} with {}", new Object[] {keyStr, content, e.getMessage()}); } return null; } @Override public int size() { try { return (int) (content.getProperties().getSize() + specialProperties.size()); } catch (RepositoryException e) { // ignore ... no rights to read properties. } return specialProperties.size(); } @Override public Set<String> keySet() { Set<String> keys = new HashSet<String>(); try { PropertyIterator props = content.getProperties(); while (props.hasNext()) { keys.add(props.nextProperty().getName()); } } catch (RepositoryException e) { // ignore - has no access } for (String name : specialProperties.keySet()) { keys.add(name); } return keys; } @Override public Set<java.util.Map.Entry<String, Object>> entrySet() { throw new UnsupportedOperationException("Entry collections are not supported"); } @Override public Collection<Object> values() { throw new UnsupportedOperationException("Value collections are not supported"); } @Override public boolean containsValue(Object arg0) { throw new UnsupportedOperationException("Value checks are not supported"); } @Override public boolean isEmpty() { // can never be empty because of the node props themselves (name, uuid, ...) return false; } @Override public void clear() { // ignore, read only } @Override public Object put(String arg0, Object arg1) { // ignore, read only return null; } @Override public void putAll(Map<? extends String, ? extends Object> arg0) { // ignore, read only } @Override public Object remove(Object arg0) { // ignore, read only return null; } public Node getJCRNode() { return content; } }