org.jahia.services.content.JCRContentUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.content.JCRContentUtils.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.content;

import com.google.common.collect.ImmutableSet;
import com.ibm.icu.text.Normalizer;
import org.apache.commons.collections.map.UnmodifiableMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.CharUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.data.GarbageCollector;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.jackrabbit.util.Text;
import org.jahia.api.Constants;
import org.jahia.bin.Jahia;
import org.jahia.data.templates.JahiaTemplatesPackage;
import org.jahia.registries.ServicesRegistry;
import org.jahia.services.SpringContextSingleton;
import org.jahia.services.content.decorator.*;
import org.jahia.services.content.nodetypes.ExtendedNodeType;
import org.jahia.services.content.nodetypes.ExtendedPropertyDefinition;
import org.jahia.services.content.nodetypes.NodeTypeRegistry;
import org.jahia.services.importexport.DocumentViewImportHandler;
import org.jahia.services.render.RenderContext;
import org.jahia.services.render.RenderService;
import org.jahia.services.render.Template;
import org.jahia.services.sites.JahiaSitesService;
import org.jahia.services.usermanager.JahiaGroupManagerService;
import org.jahia.services.usermanager.JahiaUserManagerService;
import org.jahia.settings.SettingsBean;
import org.jahia.utils.FileUtils;
import org.jahia.utils.Patterns;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.web.context.ServletContextAware;

import javax.jcr.*;
import javax.jcr.nodetype.*;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.servlet.ServletContext;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import static org.jahia.api.Constants.*;

/**
 * Utility class for accessing and manipulation JCR properties.
 *
 * @author Sergiy Shyrkov
 */
public final class JCRContentUtils implements ServletContextAware {

    public static final Pattern COLON_PATTERN = Patterns.COLON;

    public static final Comparator<NodeType> NODE_TYPE_NAME_COMPARATOR = new Comparator<NodeType>() {

        @Override
        public int compare(NodeType o1, NodeType o2) {
            return o1 == o2 ? 0 : o1.getName().compareTo(o2.getName());
        }
    };

    private static final String PREFIX = Jahia.getContextPath() + "/modules/";
    private static final Logger logger = LoggerFactory.getLogger(JCRContentUtils.class);

    private static Map<String, Boolean> iconsPresence = new ConcurrentHashMap<String, Boolean>(512, 0.8f, 32);
    private static volatile JCRContentUtils instance;

    private final Map<String, String> fileExtensionIcons;
    private final Map<String, List<String>> mimeTypes;
    private final Map<String, String> defaultUserFolderTypes;

    private NameGenerationHelper nameGenerationHelper;
    private Set<String> unsupportedMarkForDeletionNodeTypes = Collections.emptySet();
    private Pattern handleFallbackLocaleForPath;
    private ServletContext servletContext;

    /**
     * Initializes an instance of this class.
     *
     * @param mimeTypes a map with mime type mappings
     * @param fileExtensionIcons mapping between file extensions and corresponding icons
     */
    @SuppressWarnings("unchecked")
    public JCRContentUtils(Map<String, List<String>> mimeTypes, Map<String, String> fileExtensionIcons,
            Map<String, String> defaultUserFolderTypes) {
        instance = this;
        this.mimeTypes = UnmodifiableMap.decorate(mimeTypes);
        this.fileExtensionIcons = UnmodifiableMap.decorate(fileExtensionIcons);
        this.defaultUserFolderTypes = UnmodifiableMap.decorate(defaultUserFolderTypes);
    }

    public static JCRContentUtils getInstance() {
        if (instance == null) {
            throw new UnsupportedOperationException("JCRContentUtils is not initialized yet");
        }
        return instance;
    }

    /**
     * Calls the datastore garbage collector and returns the number of data entries deleted.
     *
     * @return the number of data entries deleted
     * @throws RepositoryException in case of an error
     */
    public static int callDataStoreGarbageCollector() throws RepositoryException {
        return JCRTemplate.getInstance().doExecuteWithSystemSession(new JCRCallback<Integer>() {
            public Integer doInJCR(JCRSessionWrapper session) throws RepositoryException {
                int deleted = 0;
                GarbageCollector gc = ((SessionImpl) session.getProviderSession(session.getNode("/").getProvider()))
                        .createDataStoreGarbageCollector();
                try {
                    gc.mark();
                    deleted = gc.sweep();
                    logger.info("Datastore garbage collector deleted {} data records", deleted);
                } finally {
                    gc.close();
                }
                return deleted;
            }
        });
    }

    public static boolean check(String icon) {
        String moduleId = StringUtils.substringBefore(icon, "/");
        String pathAfter = StringUtils.substringAfter(icon, "/");
        JahiaTemplatesPackage module = ServicesRegistry.getInstance().getJahiaTemplateManagerService()
                .getTemplatePackageById(moduleId);
        icon = module.getId() + "/" + module.getVersion() + "/" + pathAfter;

        Boolean present = iconsPresence.get(icon);
        if (present == null) {
            present = module.resourceExists(pathAfter + ".png");
            iconsPresence.put(icon, present);
        }
        return present;
    }

    /**
     * Removes all locks on the current node and optionally on its children.
     *
     * @param path               the path of the node remove locks from
     * @param processChildNodes  do we need to also remove locks in children?
     * @param workspace          workspace
     * @throws RepositoryException in case of an error
     */
    public static void clearAllLocks(final String path, final boolean processChildNodes, final String workspace)
            throws RepositoryException {
        JCRTemplate.getInstance().doExecuteWithSystemSessionAsUser(null, workspace, null,
                new JCRCallback<Object>() {
                    public Object doInJCR(JCRSessionWrapper session) throws RepositoryException {
                        clearAllLocks(path, processChildNodes, session);
                        return null;
                    }
                });
    }

    private static void clearAllLocks(String path, boolean processChildNodes, JCRSessionWrapper session)
            throws RepositoryException {
        JCRNodeWrapper node = session.getNode(path);
        node.clearAllLocks();
        if (processChildNodes) {
            for (NodeIterator iterator = node.getNodes(); iterator.hasNext();) {
                JCRNodeWrapper child = (JCRNodeWrapper) iterator.next();
                clearAllLocks(child.getPath(), processChildNodes, session);
            }
        }
    }

    /**
     * Creates the JCR property value, depending on the type of the corresponding value.
     *
     * @param objectValue the object value to be converted
     * @param factory     the {@link ValueFactory} instance
     * @return the JCR property value
     */
    public static Value createValue(Object objectValue, ValueFactory factory) {
        if (objectValue instanceof String) {
            return factory.createValue((String) objectValue);
        } else if (objectValue instanceof Boolean) {
            return factory.createValue((Boolean) objectValue);
        } else if (objectValue instanceof Long) {
            return factory.createValue((Long) objectValue);
        } else if (objectValue instanceof Integer) {
            return factory.createValue(((Integer) objectValue).longValue());
        } else if (objectValue instanceof Calendar) {
            return factory.createValue((Calendar) objectValue);
        } else if (objectValue instanceof Date) {
            Calendar c = new GregorianCalendar();
            c.setTime((Date) objectValue);
            return factory.createValue(c);
        } else if (objectValue instanceof byte[] || objectValue instanceof File) {
            InputStream is = null;
            try {
                is = objectValue instanceof File ? new BufferedInputStream(new FileInputStream((File) objectValue))
                        : new ByteArrayInputStream((byte[]) objectValue);
                return factory.createValue(factory.createBinary(is));
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            } finally {
                IOUtils.closeQuietly(is);
            }
        } else {
            logger.warn("Do not know, how to handle value of type {}", objectValue.getClass().getName());
        }
        return null;
    }

    /**
     * Reverse operation of encodeJCRNamePrefix.
     * Note : this is not yet complete as it depends on the restrictions imposed on XML namespaces as
     * defined here http://www.w3.org/TR/REC-xml-names/
     *
     * @param encodedPrefix
     * @return
     */
    public static String decodeJCRNamePrefix(final String encodedPrefix) {
        String originalPrefix = encodedPrefix.replace("\\3A", ":");
        return originalPrefix;
    }

    /**
     * Can be used to delete the Jackrabbit indexes folders for version and workspaces to force re-indexing on the next repository startup.
     * Note please that this method can only be invoked when the repository is not started (offline).
     *
     * @param repositoryHome the repository home folder
     */
    public static void deleteJackrabbitIndexes(File repositoryHome) {
        if (repositoryHome == null || !repositoryHome.isDirectory()) {
            return;
        }
        logger.info("Removing JCR repository indexes in repository {}", repositoryHome);

        // Remove index directories to force re-indexing on next startup
        org.apache.commons.io.FileUtils.deleteQuietly(new File(repositoryHome, "index"));
        File workspaces = new File(repositoryHome, "workspaces");
        org.apache.commons.io.FileUtils
                .deleteQuietly(new File(new File(workspaces, Constants.EDIT_WORKSPACE), "index"));
        org.apache.commons.io.FileUtils
                .deleteQuietly(new File(new File(workspaces, Constants.LIVE_WORKSPACE), "index"));

        logger.info("...done removing index folders.");
    }

    /**
     * Downloads the JCR content to a temporary file.
     *
     * @param node the JCR node with the file content
     * @return the target file descriptor
     * @throws IOException in case of an error
     */
    public static File downloadFileContent(JCRNodeWrapper node) throws IOException {
        return downloadFileContent(node, File.createTempFile("data", null));
    }

    /**
     * Downloads the JCR content to a specified file.
     *
     * @param node       the JCR node with the file content
     * @param targetFile target file to write data into
     * @return the target file descriptor
     * @throws IOException in case of an error
     */
    public static File downloadFileContent(JCRNodeWrapper node, File targetFile) throws IOException {
        InputStream is = node.getFileContent().downloadFile();
        if (is == null) {
            throw new IllegalArgumentException("Provided node has no file content");
        }
        OutputStream os = new BufferedOutputStream(new FileOutputStream(targetFile));
        try {
            IOUtils.copy(is, os);
        } finally {
            IOUtils.closeQuietly(is);
            IOUtils.closeQuietly(os);
        }
        return targetFile;
    }

    /**
     * Encode a JCR qualified form name prefix, according to the grammar defined in section 3.2.5.2
     * of the JCR 2.0 specification http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#Names
     * Note : this is not yet complete as it depends on the restrictions imposed on XML namespaces as
     * defined here http://www.w3.org/TR/REC-xml-names/
     *
     * @param originalPrefix
     * @return
     */
    public static String encodeJCRNamePrefix(final String originalPrefix) {
        String encodedPrefix = originalPrefix.replace(":", "\\3A");
        return encodedPrefix;
    }

    public static String escapeLocalNodeName(String name) {
        name = name.trim();
        StringBuilder buffer = new StringBuilder(name.length() * 2);
        for (int i = 0; i < name.length(); i++) {
            char ch = name.charAt(i);
            if (ch == '[' || ch == ']' || ch == '*' || ch == '|') {
                buffer.append('%');
                buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16)));
                buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16)));
            } else if (ch == '/' || ch == '\\' || Character.isWhitespace(ch)) {
                if (buffer.length() > 0) {
                    buffer.append(' ');
                }
            } else {
                buffer.append(ch);
            }
        }
        return buffer.toString();
    }

    public static String escapeNodePath(String path) {
        StringBuilder buffer = new StringBuilder(path.length() * 2);
        for (int i = 0; i < path.length(); i++) {
            char ch = path.charAt(i);
            if (ch == '[' || ch == ']' || ch == '*' || ch == '|') {
                buffer.append('%');
                buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16)));
                buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16)));
            } else {
                buffer.append(ch);
            }
        }
        return buffer.toString();
    }

    /**
     * Returns the next available name for a node, appending if needed numbers.
     *
     * @param dest the destination node, where the new one will be created
     * @param name the name of the new node
     * @return the next available name for a node, appending if needed numbers
     */
    public static String findAvailableNodeName(Node dest, String name) {
        return findAvailableNodeName(dest, name, true);
    }

    /**
     * Returns the next available name for a node, appending if needed numbers.
     *
     * @param dest the destination node, where the new one will be created
     * @param name the name of the new node
     * @param hasExtension <code>true</code> if the name has an extension (e.g. in case of file names)
     * @return the next available name for a node, appending if needed numbers
     */
    public static String findAvailableNodeName(Node dest, String name, boolean hasExtension) {
        try {
            dest.getNode(name);
        } catch (RepositoryException e) {
            return name;
        }

        int i = 1;

        String basename = name;
        String ext = "";
        if (hasExtension) {
            int dot = basename.lastIndexOf('.');
            if (dot > 0) {
                ext = basename.substring(dot);
                basename = basename.substring(0, dot);
            }
        }
        int und = basename.lastIndexOf('-');
        if (und > -1 && Patterns.NUMBERS.matcher(basename.substring(und + 1)).matches()) {
            basename = basename.substring(0, und);
        }

        do {
            try {
                String newSuffix = hasExtension ? ("-" + (i++) + ext) : ("-" + (i++));
                name = basename + newSuffix;
                //name has a sizelimit of 32 chars
                int maxNameSize = SettingsBean.getInstance().getMaxNameSize();
                if (name.length() > maxNameSize) {
                    name = basename.substring(0,
                            (basename.length() <= maxNameSize ? basename.length() : maxNameSize)
                                    - newSuffix.length())
                            + newSuffix;
                }
                dest.getNode(name);
            } catch (RepositoryException e) {
                break;
            }

        } while (true);

        return name;
    }

    public static JCRNodeWrapper findDisplayableNode(JCRNodeWrapper node, RenderContext context) {
        Template template = null;
        JCRNodeWrapper currentNode = node;
        try {
            while (template == null && currentNode != null) {
                template = RenderService.getInstance().resolveTemplate(new org.jahia.services.render.Resource(
                        currentNode, "html", null, org.jahia.services.render.Resource.CONFIGURATION_PAGE), context);
                if (template == null) {
                    currentNode = currentNode.getParent();
                }
            }
        } catch (Exception e) {
            currentNode = null;
        }
        return currentNode;
    }

    public static JCRNodeWrapper findDisplayableNode(JCRNodeWrapper node, RenderContext context,
            JCRSiteNode contextSite) {
        if (contextSite != null) {
            final JCRSiteNode old = context.getSite();
            context.setSite(contextSite);
            final JCRNodeWrapper displayableNode = JCRContentUtils.findDisplayableNode(node, context);
            context.setSite(old);
            return displayableNode;
        } else {
            return JCRContentUtils.findDisplayableNode(node, context);
        }
    }

    public static String generateNodeName(String text) {
        return generateNodeName(text, SettingsBean.getInstance().getMaxNameSize());
    }

    /**
     * Generates the JCR node name from the provided text by normalizing it,
     * converting to lower case, replacing spaces with dashes and truncating to
     * ${@code maxLength} characters.
     *
     * @param text      the original text to be used as a source
     * @param maxLength the maximum length of the resulting name (it will be
     *                  truncated if needed)
     * @return the JCR node name from the provided text by normalizing it,
     *         converting to lower case, replacing spaces with dashes and
     *         truncating to ${@code maxLength} characters
     */
    public static String generateNodeName(String text, int maxLength) {
        String nodeName = text;
        final char[] chars = Normalizer.normalize(nodeName, Normalizer.NFKD).toCharArray();
        final char[] newChars = new char[chars.length];
        int j = 0;
        for (char aChar : chars) {
            if (CharUtils.isAsciiAlphanumeric(aChar) || aChar == 32 || aChar == '-') {
                newChars[j++] = aChar;
            }
        }
        nodeName = Patterns.SPACE.matcher(new String(newChars, 0, j).trim()).replaceAll("-").toLowerCase();
        if (nodeName.length() > maxLength) {
            nodeName = nodeName.substring(0, maxLength);
            if (nodeName.endsWith("-") && nodeName.length() > 2) {
                nodeName = nodeName.substring(0, nodeName.length() - 1);
            }
        }

        return StringUtils.isNotEmpty(nodeName) ? nodeName : "untitled";
    }

    /**
     * Returns a set of all mixin types, which can be added to the provided node.
     *
     * @param node the node to add mixin types to
     * @return a set of all mixin types, which can be added to the provided node
     * @throws RepositoryException in case of a repository access error
     */
    public static Set<String> getAssignableMixins(JCRNodeWrapper node) throws RepositoryException {
        Set<String> mixins = new TreeSet<String>();
        Set<String> existingMixins = new HashSet<String>(node.getNodeTypes());
        existingMixins.add("mix:shareable"); // we will skip this one

        NodeTypeIterator allMixins = node.getSession().getWorkspace().getNodeTypeManager().getMixinNodeTypes();
        while (allMixins.hasNext()) {
            String nt = allMixins.nextNodeType().getName();
            if (!existingMixins.contains(nt) && node.canAddMixin(nt)) {
                mixins.add(nt);
            }
        }

        return mixins;
    }

    /**
     * Returns a list of child nodes of the provided node, matching the specified node type (multiple node types can be specified, separated
     * by a comma).
     *
     * @param node the parent node to retrieve children from
     * @param type the node type to be matched by a retrieved child. Multiple node types can be specified, separated by a comma.
     * @return a list of child nodes of the provided node, matching the specified node type (multiple node types can be specified, separated
     *         by a comma).
     */
    public static List<JCRNodeWrapper> getChildrenOfType(JCRNodeWrapper node, String type) {
        return getChildrenOfType(node, type, 0);
    }

    public static List<JCRNodeWrapper> getChildrenOfType(JCRNodeWrapper node, String type, int limit) {
        List<JCRNodeWrapper> children = null;
        if (node == null) {
            return null;
        }
        if (type.contains(",")) {
            String[] typesToCheck = Patterns.COMMA.split(type);
            List<JCRNodeWrapper> matchingChildren = new LinkedList<JCRNodeWrapper>();
            try {
                for (NodeIterator iterator = node.getNodes(); iterator.hasNext();) {
                    if (limit > 0 && matchingChildren.size() == limit) {
                        break;
                    }
                    Node child = iterator.nextNode();
                    for (String matchType : typesToCheck) {
                        if (child.isNodeType(matchType)) {
                            matchingChildren.add((JCRNodeWrapper) child);
                            break;
                        }
                    }
                }
            } catch (RepositoryException e) {
                logger.warn(e.getMessage(), e);
            }
            children = matchingChildren;
        } else {
            children = getNodes(node, type, limit);
        }
        return children;
    }

    /**
     * Returns a content object key for a node if available. Otherwise returns
     * node name.
     *
     * @param node the node to get name
     * @return a content object key for a node if available. Otherwise returns
     *         node name
     * @throws RepositoryException
     */
    public static String getContentNodeName(Node node) throws RepositoryException {
        return node.getName();
    }

    /**
     * Returns a node path, composed using only content object keys, i.e.
     * <p/>
     * <code>/mySite/ContentPage_1/ContentPage_19/ContentContainerList_21/ContentContainer_13</code>
     *
     * @param node the content node to compute path for
     * @return a node path, composed using only content object keys
     * @throws RepositoryException
     */
    public static String getContentObjectPath(Node node) throws RepositoryException {

        StringBuilder path = new StringBuilder(64);
        path.append("/").append(getContentNodeName(node));
        Node parent = null;
        try {
            parent = node.getParent();
        } catch (ItemNotFoundException e) {
            parent = null;
        }
        while (parent != null) {
            String name = getContentNodeName(parent);
            path.insert(0, name);
            if (!"/".equals(name) && name.length() > 0) {
                path.insert(0, "/");
            }
            try {
                parent = parent.getParent();
            } catch (ItemNotFoundException e) {
                parent = null;
            }
        }

        return path.toString();
    }

    public static NodeIterator getDescendantNodes(JCRNodeWrapper node, String type) {
        try {
            return node.getSession().getWorkspace().getQueryManager()
                    .createQuery("select * from [" + type + "] as sel where isdescendantnode(sel,['"
                            + JCRContentUtils.sqlEncode(node.getPath()) + "'])", Query.JCR_SQL2)
                    .execute().getNodes();
        } catch (InvalidQueryException e) {
            logger.error("Error while retrieving nodes", e);
        } catch (RepositoryException e) {
            logger.error("Error while retrieving nodes", e);
        }
        return NodeIteratorImpl.EMPTY;
    }

    /**
     * Get the node or property display name depending on the locale
     *
     * @param item                      the item to get the label for
     * @param locale                    current locale
     * @param nodeTypeForSearchingLabel
     * @return the node or property display name depending on the locale
     */
    public static String getDisplayLabel(Object item, Locale locale, ExtendedNodeType nodeTypeForSearchingLabel) {
        if (item != null) {

            try {
                // case of property
                if (item instanceof Property) {
                    Property property = (Property) item;
                    PropertyDefinition propertyDefintion = property.getDefinition();
                    if (propertyDefintion != null && propertyDefintion instanceof ExtendedPropertyDefinition) {
                        ExtendedPropertyDefinition itemDef = (ExtendedPropertyDefinition) propertyDefintion;
                        return itemDef.getLabel(locale, nodeTypeForSearchingLabel);
                    } else {
                        logger.error(
                                "PropertyDefinition doesn't implement 'org.jahia.services.content.nodetypes.ExtendedPropertyDefinition'");
                    }
                }
                // case of PropertyDefinition
                else if (item instanceof PropertyDefinition) {
                    if (item instanceof ExtendedPropertyDefinition) {
                        ExtendedPropertyDefinition itemDef = (ExtendedPropertyDefinition) item;
                        return itemDef.getLabel(locale, nodeTypeForSearchingLabel);
                    } else {
                        logger.error(
                                "PropertyDefinition doesn't implement 'org.jahia.services.content.nodetypes.ExtendedPropertyDefinition'");
                    }
                }
                // case of node type
                else if (item instanceof NodeType) {
                    NodeType nodeType = (NodeType) item;
                    if (nodeType instanceof ExtendedNodeType) {
                        ExtendedNodeType extendNodeType = (ExtendedNodeType) nodeType;
                        return extendNodeType.getLabel(locale);
                    } else {
                        logger.error(
                                "nodeType doesn't implement 'org.jahia.services.content.nodetypes.ExtendedNodeType'");
                    }
                } else {
                    logger.error("Object must be a 'javax.jcr.Property' or 'javax.jcr.nodetype.NodeType'");
                }
            } catch (RepositoryException e) {
                logger.error(e.getMessage(), e);
            }
        }
        return null;
    }

    public static String getExpandedName(String name, NamespaceRegistry namespaceRegistry)
            throws RepositoryException {
        if (!name.startsWith("{")) {
            if (name.contains(":")) {
                name = "{" + namespaceRegistry.getURI(StringUtils.substringBefore(name, ":")) + "}"
                        + StringUtils.substringAfter(name, ":");
            } else {
                name = "{}" + name;
            }
        }

        return name;
    }

    public static String getJCRName(String qualifiedName, NamespaceRegistry namespaceRegistry)
            throws RepositoryException {
        if (qualifiedName.startsWith("{")) {
            final String uri = StringUtils.substringBetween(qualifiedName, "{", "}");
            if (uri.isEmpty()) {
                qualifiedName = StringUtils.substringAfter(qualifiedName, "}");
            } else {
                qualifiedName = namespaceRegistry.getPrefix(uri) + ":"
                        + StringUtils.substringAfter(qualifiedName, "}");
            }
        }

        return qualifiedName;
    }

    public static String getIconWithContext(ExtendedNodeType type) throws RepositoryException {
        String icon = getIcon(type, null);
        if (icon == null) {
            icon = getIcon(NodeTypeRegistry.getInstance().getNodeType("jmix:droppableContent"));
        }
        return PREFIX + icon;
    }

    /**
     * Returns the icon path for the specified type
     * @param type wanted type
     * @return Path of the icon
     * @throws RepositoryException
     */
    public static String getIcon(ExtendedNodeType type) throws RepositoryException {
        return getIcon(type, null);
    }

    private static String getIcon(ExtendedNodeType type, String subType) throws RepositoryException {
        String icon = getIconsFolder(type) + replaceColon(type.getName())
                + (StringUtils.isEmpty(subType) ? "" : "_" + subType);
        if (check(icon)) {
            return icon;
        } else if (!StringUtils.isEmpty(subType)) {
            icon = getIconsFolder(type) + replaceColon(type.getName());
            if (check(icon)) {
                return icon;
            }
        }
        for (ExtendedNodeType nodeType : type.getSupertypes()) {
            icon = getIconsFolder(nodeType) + replaceColon(nodeType.getName());
            if (check(icon)) {
                return icon;
            }
        }
        return null;
    }

    public static String getIcon(JCRNodeWrapper f) throws RepositoryException {
        return getIconWithContext(f, false);
    }

    public static String getIconWithContext(JCRNodeWrapper f, boolean useContext) throws RepositoryException {
        ExtendedNodeType primaryNodeType = f.getPrimaryNodeType();
        String folder = getIconsFolder(primaryNodeType);
        if (f.isNodeType("jmix:hasIcon") && f.hasProperty("j:icon")) {
            return ((JCRFileNode) f.getProperty("j:icon").getNode()).getUrl();
        }

        if (f.isFile()) {
            return (useContext ? PREFIX : "") + folder + "jnt_file_" + FileUtils.getFileIcon(f.getName());
        } else if (f.isPortlet()) {
            return (useContext ? PREFIX : "") + folder + "jnt_portlet";
        } else if (f instanceof JCRComponentNode) {
            String type = f.getName();
            ExtendedNodeType nt = primaryNodeType;
            if (!"components".equals(type)) {
                try {
                    nt = NodeTypeRegistry.getInstance().getNodeType(type);
                } catch (NoSuchNodeTypeException e) {

                }
            }
            return (useContext ? PREFIX : "") + getIcon(nt, getSubType(nt, f));
        } else if (!f.getProvider().isDefault() && f.isNodeType("jnt:folder")) {
            return (useContext ? PREFIX : "") + folder + "remoteFolder";
        } else {
            return (useContext ? PREFIX : "") + getIcon(primaryNodeType, getSubType(primaryNodeType, f));
        }
    }

    private static String getSubType(ExtendedNodeType nt, JCRNodeWrapper f) {
        String subType = null;
        try {
            if (Constants.JAHIANT_VIRTUALSITE.equals(nt.getName()) && f.hasProperty(Constants.SITETYPE)) {
                subType = f.getPropertyAsString(Constants.SITETYPE);
            }
        } catch (RepositoryException e) {
        }
        return subType;
    }

    /**
     * For a nodetype gets the folder that contains its icon
     * for system and module default nodetypes, icons asset folder is returned
     * @param primaryNodeType
     * @return folder
     * @throws RepositoryException
     */
    public static String getIconsFolder(final ExtendedNodeType primaryNodeType) throws RepositoryException {
        String systemId = primaryNodeType.getSystemId();
        JahiaTemplatesPackage aPackage = !systemId.startsWith("system-")
                && !JahiaTemplatesPackage.ID_DEFAULT.equals(systemId)
                        ? ServicesRegistry.getInstance().getJahiaTemplateManagerService()
                                .getTemplatePackageById(systemId)
                        : null;

        return aPackage != null ? aPackage.getId() + "/icons/" : "assets/icons/";
    }

    /**
     * Returns the parsed lock type from the provided token.
     *
     * @param lockTypeToken the token to detect lock type from
     * @return the parsed lock type from the provided token
     */
    public static JCRNodeLockType getLockType(String lockTypeToken) {
        JCRNodeLockType type = JCRNodeLockType.UNKNOWN;
        if (lockTypeToken != null && lockTypeToken.length() > 1) {
            if (lockTypeToken.charAt(0) == ' ') {
                // system or process type lock
                if (lockTypeToken.startsWith(MARKED_FOR_DELETION_LOCK_USER)) {
                    type = JCRNodeLockType.DELETION;
                } else if (lockTypeToken.endsWith(":validation")) {
                    type = JCRNodeLockType.WORKFLOW;
                }
            } else {
                // user lock
                type = JCRNodeLockType.USER;
            }
        }

        return type;
    }

    /**
     * Returns a set of lock types for the current node. If there are no locks found in the lock infos, returns an empty set.
     *
     * @param lockInfos the lock information of a node
     * @return a set of lock types for the current node. If there are no locks found in the lock infos, returns an empty set.
     */
    public static Set<JCRNodeLockType> getLockTypes(Map<String, List<String>> lockInfos) {
        Set<JCRNodeLockType> types = Collections.emptySet();
        if (lockInfos != null && !lockInfos.isEmpty()) {
            types = new HashSet<JCRNodeLockType>(4);
            for (List<String> infos : lockInfos.values()) {
                for (String lockToken : infos) {
                    types.add(getLockType(lockToken));
                }
            }
        }

        return types;
    }

    /**
     * Returns the last part of a path, so the last name. This method supports expanded form names in the path, so the
     * last slash detection will be properly handled.
     *
     * @param path the path to get the name from
     * @return the name of the leaf in the path.
     */
    public static String getNameFromPath(String path) {
        String[] pathNames = splitJCRPath(path);
        return pathNames[pathNames.length - 1];
    }

    public static List<JCRNodeWrapper> getNodes(JCRNodeWrapper node, String type) {
        return getNodes(node, type, 0);
    }

    public static List<JCRNodeWrapper> getNodes(JCRNodeWrapper node, String type, int limit) {
        try {
            List<JCRNodeWrapper> res = new ArrayList<JCRNodeWrapper>();
            NodeIterator ni = node.getNodes();
            while (ni.hasNext()) {
                if (limit > 0 && res.size() == limit) {
                    break;
                }
                JCRNodeWrapper child = (JCRNodeWrapper) ni.next();
                if (StringUtils.isEmpty(type) || child.isNodeType(type)) {
                    res.add(child);
                }
            }
            return res;
            //            return node.getSession().getWorkspace().getQueryManager().createQuery("select * from ["+type+"] as sel where ischildnode(sel,['"+node.getPath()+"'])",
            //                                                                                  Query.JCR_SQL2).execute().getNodes();
            //        } catch (InvalidQueryException e) {
            //            logger.error("Error while retrieving nodes", e);
        } catch (RepositoryException e) {
            logger.error("Error while retrieving nodes", e);
        }
        return new LinkedList<JCRNodeWrapper>();
    }

    /**
     * Little utility method to retrieve or create a path, building it if necessary. For example let's say that we want
     * to get or create the path from a parentNode : messages/inbox . We can simply pass the parent node, the session
     * and the path to check, and it will either retrieve it if it exists, or create it if it doesn't.
     * <p/>
     * Please note that this method also checks out the parent nodes.
     *
     * @param session
     * @param parentNode   the parent node in from which we want to retrieve the relative path.
     * @param path         the path to retrieve or create. Note that this path MUST be relative
     * @param pathNodeType the type to use for the intermediary nodes (and the last path too !) if they need to be
     *                     created. Usually you'll want to use Constants.JAHIANT_CONTENTLIST here.
     * @return the leaf that is equivalent to the lowest path value.
     * @throws RepositoryException occurs if there is any problem accessing content or creating the nodes.
     */
    public static JCRNodeWrapper getOrAddPath(JCRSessionWrapper session, JCRNodeWrapper parentNode, String path,
            String pathNodeType) throws RepositoryException {
        String[] subPaths = splitJCRPath(path);
        JCRNodeWrapper node = parentNode;
        for (String subPath : subPaths) {
            if (StringUtils.isNotBlank(subPath) && !"*".equals(subPath)) {
                try {
                    node = node.getNode(subPath);
                    session.checkout(node);
                } catch (PathNotFoundException e) {
                    if (node != null) {
                        session.checkout(node);
                        node = node.addNode(subPath, pathNodeType);
                    }
                }
            }
        }
        return node;
    }

    /**
     * A small utility method to retrieve the parent path of a path that contains expanded form names.
     *
     * @param path the path for which we want to retrieve the parent path.
     * @return the parent path including all names in expanded or qualified form.
     */
    public static String getParentJCRPath(String path) {
        String[] pathNames = splitJCRPath(path);
        StringBuilder parentPath = new StringBuilder();
        // if we are dealing with an absolute path, we add the initial separator
        if (path.startsWith("/")) {
            parentPath.append("/");
        }
        for (int i = 0; i < pathNames.length - 1; i++) {
            parentPath.append(pathNames[i]);
            if (i < pathNames.length - 2) {
                parentPath.append("/");
            }
        }
        return parentPath.toString();
    }

    public static JCRNodeWrapper getParentOfType(JCRNodeWrapper node, String type) {
        JCRNodeWrapper matchingParent = null;
        try {
            JCRNodeWrapper parent = node.getParent();
            while (parent != null) {
                if (parent.isNodeType(type)) {
                    matchingParent = parent;
                    break;
                }
                parent = parent.getParent();
            }
        } catch (ItemNotFoundException e) {
            // we reached the hierarchy top
        } catch (RepositoryException e) {
            logger.error("Error while retrieving nodes parent node. Cause: " + e.getMessage(), e);
        }
        return matchingParent;
    }

    public static JCRNodeWrapper getPathFolder(JCRNodeWrapper root, String name, String options, String nodeType)
            throws RepositoryException {
        JCRNodeWrapper result = root;
        if (options.contains("initials")) {
            String s = "" + Character.toUpperCase(name.charAt(0));
            if (!result.hasNode(s)) {
                result = result.addNode(s, nodeType);
            } else {
                result = result.getNode(s);
            }
        }
        return result;
    }

    private static ExtendedPropertyDefinition getPropertyDefExtension(PropertyDefinition propDef) {
        try {
            return NodeTypeRegistry.getInstance().getNodeType(propDef.getDeclaringNodeType().getName())
                    .getPropertyDefinition(propDef.getName());
        } catch (NoSuchNodeTypeException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    public static PropertyDefinition getPropertyDefinition(NodeType type, String property)
            throws RepositoryException {
        PropertyDefinition foundDefintion = null;
        PropertyDefinition[] pds = type.getDeclaredPropertyDefinitions();
        for (int i = 0; i < pds.length; i++) {
            PropertyDefinition pd = pds[i];
            if (pd.getName().equals(property)) {
                foundDefintion = pd;
                break;
            }
        }

        return foundDefintion;
    }

    public static PropertyDefinition getPropertyDefinition(String nodeType, String property)
            throws RepositoryException {
        return getPropertyDefinition(NodeTypeRegistry.getInstance().getNodeType(nodeType), property);
    }

    public static int getPropertyDefSelector(ItemDefinition itemDef) {
        ExtendedPropertyDefinition propDefExtension = null;
        if (itemDef instanceof PropertyDefinition) {
            propDefExtension = getPropertyDefExtension((PropertyDefinition) itemDef);
        }

        return propDefExtension != null ? propDefExtension.getSelector() : 0;
    }

    /**
     * If the node path contains site information (i.e.
     * <code>/sites/&lt;siteKey&gt;/...</code>) this method returns the site key
     * part; otherwise <code>null</code> is returned.
     *
     * @param jcrNodePath the JCR node path
     * @return if the node path contains site information (i.e.
     *         <code>/sites/&lt;siteKey&gt;/...</code>) this method returns the
     *         site key part; otherwise <code>null</code> is returned
     */
    public static String getSiteKey(String jcrNodePath) {
        return jcrNodePath != null
                ? (jcrNodePath.startsWith("/sites/") ? StringUtils.substringBetween(jcrNodePath, "/sites/", "/")
                        : null)
                : null;
    }

    public static String getSystemSitePath() {
        return "/sites/" + JahiaSitesService.SYSTEM_SITE_KEY;
    }

    /**
     * Returns an object value that corresponds to the provided JCR property value depending on its type.
     *
     * @param propertyValue the JCR property value to be converted
     * @return the object value
     * @throws RepositoryException  in case of a conversion error
     * @throws ValueFormatException in case of a conversion error
     */
    public static Object getValue(Value propertyValue) throws ValueFormatException, RepositoryException {
        Object value = propertyValue.getString();
        switch (propertyValue.getType()) {
        case PropertyType.BOOLEAN:
            value = Boolean.valueOf(propertyValue.getBoolean());
            break;
        case PropertyType.DATE:
            value = propertyValue.getDate();
            break;
        case PropertyType.DECIMAL:
        case PropertyType.LONG:
            value = Long.valueOf(propertyValue.getDecimal().longValue());
            break;
        case PropertyType.DOUBLE:
            value = Double.valueOf(propertyValue.getDouble());
            break;
        }
        return value;
    }

    /**
     * Used by portlet backends to determine if a user is part of a specific permissionName on a node specified by it's
     * UUID
     *
     * @param workspaceName  the name of the workspace in which we load the node from.
     * @param permissionName
     * @param nodeUUID
     * @return
     */
    public static boolean hasPermission(String workspaceName, final String permissionName, final String nodeUUID) {
        try {
            JCRSessionWrapper session = JCRTemplate.getInstance().getSessionFactory()
                    .getCurrentUserSession(workspaceName);
            JCRNodeWrapper node = session.getNodeByIdentifier(nodeUUID);
            return node.hasPermission(permissionName);
        } catch (Exception e) {
            logger.error("Error while checking permission " + permissionName + " for node UUID " + nodeUUID, e);
        }
        return false;
    }

    /**
     * Performs import of JCR data using provided skeleton locations. This method is used when a new virtual site or a new user is created.
     *
     * @param skeletonLocations the (pattern-based) location to search for resources. Multiple locations can be provided separated by comma (or any
     *                          delimiter, defined in {@link org.springframework.context.ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS} )
     * @param targetPath        target JCR path to perform import into
     * @param session           the current JCR session
     * @throws IOException                    in case of skeleton lookup error
     * @throws InvalidSerializedDataException import related exception
     * @throws RepositoryException            general JCR exception
     */
    public static void importSkeletons(String skeletonLocations, String targetPath, JCRSessionWrapper session)
            throws IOException, InvalidSerializedDataException, RepositoryException {
        importSkeletons(skeletonLocations, targetPath, session, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, null);
    }

    /**
     * Performs import of JCR data using provided skeleton locations. This method is used when a new virtual site or a new user is created.
     *
     * @param skeletonLocations  the (pattern-based) location to search for resources. Multiple locations can be provided separated by comma (or any
     *                           delimiter, defined in {@link org.springframework.context.ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS} )
     * @param targetPath         target JCR path to perform import into
     * @param session            the current JCR session
     * @param importUUIDBehavior the {@link javax.jcr.ImportUUIDBehavior} to use during import
     * @param replacements
     * @throws IOException                    in case of skeleton lookup error
     * @throws InvalidSerializedDataException import related exception
     * @throws RepositoryException            general JCR exception
     */
    public static void importSkeletons(String skeletonLocations, String targetPath, JCRSessionWrapper session,
            int importUUIDBehavior, Map<String, String> replacements)
            throws IOException, InvalidSerializedDataException, RepositoryException {
        for (Resource resource : SpringContextSingleton.getInstance().getResources(skeletonLocations)) {
            logger.info("Importing data using skeleton [{}]", resource);
            InputStream is = null;
            try {
                is = resource.getInputStream();
                session.importXML(targetPath, is, importUUIDBehavior,
                        DocumentViewImportHandler.ROOT_BEHAVIOUR_IGNORE, replacements, null);
            } finally {
                IOUtils.closeQuietly(is);
            }
        }
    }

    /**
     * Performs import of JCR data using provided skeleton locations. This method is used when a new virtual site or a new user is created.
     *
     * @param skeletonLocations the (pattern-based) location to search for resources. Multiple locations can be provided separated by comma (or any
     *                          delimiter, defined in {@link org.springframework.context.ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS} )
     * @param targetPath        target JCR path to perform import into
     * @param session           the current JCR session
     * @param replacements
     * @throws IOException                    in case of skeleton lookup error
     * @throws InvalidSerializedDataException import related exception
     * @throws RepositoryException            general JCR exception
     */
    public static void importSkeletons(String skeletonLocations, String targetPath, JCRSessionWrapper session,
            Map<String, String> replacements)
            throws IOException, InvalidSerializedDataException, RepositoryException {
        importSkeletons(skeletonLocations, targetPath, session, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW,
                replacements);
    }

    public static boolean isADisplayableNode(JCRNodeWrapper node, RenderContext context) {
        Template template = null;
        JCRNodeWrapper currentNode = node;
        try {
            template = RenderService.getInstance().resolveTemplate(new org.jahia.services.render.Resource(
                    currentNode, "html", null, org.jahia.services.render.Resource.CONFIGURATION_PAGE), context);
            return template != null;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Checks if the specified mime type belongs to one of the specified groups (like pdf,word,openoffice, etc.).
     *
     * @param mimeType      the mime type to be checked
     * @param mimeTypeGroup the group (or multiple groups, separated by comma) the specified mime type should belong to
     * @return if the specified mime type belongs to one of the specified groups (like pdf,word,openoffice, etc.)
     */
    public static boolean isMimeTypeGroup(String mimeType, String mimeTypeGroup) {
        return isMimeTypeGroup(mimeType, StringUtils.split(mimeTypeGroup, ", ;|"));
    }

    /**
     * Checks if the specified mime type belongs to one of the specified groups (like pdf,word,openoffice, etc.).
     *
     * @param mimeType       the mime type to be checked
     * @param mimeTypeGroups the groups the specified mime type should belong to
     * @return if the specified mime type belongs to one of the specified groups (like pdf,word,openoffice, etc.)
     */
    public static boolean isMimeTypeGroup(String mimeType, String... mimeTypeGroups) {
        if (mimeType == null) {
            return false;
        }

        boolean found = false;
        for (String grp : mimeTypeGroups) {
            List<String> mimeTypes = getInstance().getMimeTypes().get(grp);
            if (mimeTypes == null) {
                continue;
            }
            for (String mime : mimeTypes) {
                if (mime.contains("*")) {
                    found = Pattern.matches(StringUtils.replace(StringUtils.replace(mime, ".", "\\."), "*", ".*"),
                            mimeType);
                } else {
                    found = mime.equals(mimeType);
                }
                if (found) {
                    break;
                }
            }
            if (found) {
                break;
            }
        }

        return found;
    }

    /**
     * Checks if the specified language is marked as invalid for displaying.
     *
     * @param node
     *            the node to be checked
     * @param languageCode
     *            language code to be tested
     * @return <code>true</code> if the specified language is in the list of languages marked as invalid for displaying; <code>false</code>
     *         otherwise.
     * @throws RepositoryException
     *             in case of an exception
     */
    public static boolean isLanguageInvalid(Node node, String languageCode) throws RepositoryException {
        boolean invalid = false;
        if (node.hasProperty("j:invalidLanguages")) {
            final Value[] values = node.getProperty("j:invalidLanguages").getValues();
            for (Value value : values) {
                if (value.getString().equals(languageCode)) {
                    invalid = true;
                    break;
                }
            }
        }

        return invalid;
    }

    /**
     * Returns <code>true</code> if the node is locked and cannot be edited by current user.
     *
     * @param node the node to be tested
     * @return <code>true</code> if the node is locked and cannot be edited by current user
     * @throws RepositoryException in case of a JCR error
     */
    public static boolean isLockedAndCannotBeEdited(JCRNodeWrapper node) throws RepositoryException {
        if (node == null) {
            return false;
        }

        String username = node.getSession().getUser().getName();
        String lockOwner = node.getLockOwner();
        boolean isLocked = node.isLocked() && (lockOwner == null || !lockOwner.equals(username));
        try {
            if (!isLocked && node.hasProperty("j:lockTypes")) {
                Value[] values = node.getProperty("j:lockTypes").getValues();
                for (Value value : values) {
                    if (!value.getString().startsWith(username)) {
                        isLocked = true;
                        break;
                    }
                }
            }
        } catch (PathNotFoundException e) {
            logger.debug("concurrency issue lock is not present anymore");
        }

        return isLocked;
    }

    /**
     * Returns <code>true</code> if the provided node matches one of the specified node types.
     *
     * @param node  the node to be tested
     * @param types an array of node types to be matched.
     * @return <code>true</code> if the provided node matches one of the specified node types
     * @throws RepositoryException in case of a JCR error
     */
    public static boolean isNodeType(JCRNodeWrapper node, Iterable<String> types) throws RepositoryException {
        if (node == null || types == null) {
            return false;
        }
        boolean matches = false;
        for (String matchType : types) {
            if (node.isNodeType(matchType)) {
                matches = true;
                break;
            }
        }

        return matches;
    }

    /**
     * Returns <code>true</code> if the provided node matches the specified node type (multiple node types can be specified, separated by a
     * comma).
     *
     * @param node the node to be tested
     * @param type the node type to be matched. Multiple node types can be specified, separated by a comma.
     * @return <code>true</code> if the provided node matches the specified node type (multiple node types can be specified, separated by a
     *         comma).
     * @throws RepositoryException in case of a JCR error
     */
    public static boolean isNodeType(JCRNodeWrapper node, String type) throws RepositoryException {
        if (node == null || StringUtils.isEmpty(type)) {
            return false;
        }
        boolean matches = false;
        if (type.contains(",")) {
            String[] types = Patterns.COMMA.split(type);
            for (String matchType : types) {
                if (node.isNodeType(matchType)) {
                    matches = true;
                    break;
                }
            }
        } else {
            matches = node.isNodeType(type);
        }

        return matches;
    }

    /**
     * Returns <code>true</code> if the provided UUID string does not seem like a valid Jackrabbit node UUID. In such a case it comes from a
     * different provide, like VFS.
     *
     * @param uuid the UUID string to check
     * @return code>true</code> if the provided UUID string does not seem like a valid Jackrabbit node UUID. In such a case it comes from a
     *         different provide, like VFS
     * @deprecated without any replacement
     */
    @Deprecated
    public static boolean isNotJcrUuid(String uuid) {
        return StringUtils.isEmpty(uuid) || uuid.contains("/");
    }

    public static boolean isValidFilename(String name) {
        return (!name.startsWith(" ") && !name.endsWith(" ") && name.matches("([^\\*:/\\\\<>|?\"])*"));
    }

    /**
     * Validates if the specified name is a valid JCR workspace (either <code>default</code> or <code>live</code>).
     *
     * @param workspace the workspace name to check
     * @return <code>true</code> if the specified name is a valid JCR workspace (either <code>default</code> or <code>live</code>);
     *         otherwise returns <code>false</code>
     */
    public static final boolean isValidWorkspace(String workspace) {
        return isValidWorkspace(workspace, false);
    }

    /**
     * Validates if the specified name is a valid JCR workspace (either <code>default</code> or <code>live</code>).
     *
     * @param workspace  the workspace name to check
     * @param allowBlank set to true if the workspace name is allowed to be null or empty
     * @return <code>true</code> if the specified name is a valid JCR workspace (either <code>default</code> or <code>live</code>);
     *         otherwise returns <code>false</code>
     */
    public static final boolean isValidWorkspace(String workspace, boolean allowBlank) {
        return StringUtils.isEmpty(workspace) ? allowBlank
                : EDIT_WORKSPACE.equals(workspace) || LIVE_WORKSPACE.equals(workspace);
    }

    /**
     * Small utility method to help with proper namespace registration in all JCR providers.
     *
     * @param session
     * @param prefix
     * @param uri
     * @throws RepositoryException
     */
    public static void registerNamespace(Session session, String prefix, String uri) throws RepositoryException {
        NamespaceRegistry namespaceRegistry = session.getWorkspace().getNamespaceRegistry();
        Set<String> prefixes = ImmutableSet.copyOf(namespaceRegistry.getPrefixes());
        if (!prefixes.contains(prefix)) {
            namespaceRegistry.registerNamespace(prefix, uri);
            session.setNamespacePrefix(prefix, uri);
        }

    }

    public static String replaceColon(String name) {
        return name != null ? StringUtils.replace(name, ":", "_") : name;
    }

    /**
     * Returns the number of elements in the provided iterator.
     *
     * @param iterator the item iterator to check the size
     * @return the number of elements in the provided iterator
     */
    public static long size(RangeIterator iterator) {
        long size = iterator.getSize();
        if (size <= 0) {
            size = 0;
            while (iterator.hasNext()) {
                size++;
                iterator.next();
            }
        }

        return size;
    }

    /**
     * Utility method to split a JCR path into names. Note that this method supports expanded name notation (using
     * URIs), such as {http://www.jcp.org/jcr/1.0}read, as it is tricky to split simply using the "/" character when
     * URIs are present.
     *
     * @param path a relative or absolute path, with node names in qualified or expanded form
     * @return an array of String instances that contain the node names in order of the path.
     */
    public static String[] splitJCRPath(String path) {
        List<String> result = new ArrayList<String>();
        int pathPos = 0;

        if (path.startsWith("/")) {
            pathPos++;
        }

        int nextSlashPos = -1;
        do {
            StringBuilder currentName = new StringBuilder();
            if (path.indexOf('{', pathPos) == pathPos) {
                int endingBracketPos = path.indexOf('}', pathPos + 1);
                currentName.append(path.substring(pathPos, endingBracketPos + 1));
                pathPos = endingBracketPos + 1;
            }
            nextSlashPos = path.indexOf('/', pathPos);
            if (nextSlashPos > -1) {
                currentName.append(path.substring(pathPos, nextSlashPos));
                pathPos = nextSlashPos + 1;
            } else {
                currentName.append(path.substring(pathPos));
            }
            result.add(currentName.toString());
        } while (nextSlashPos > -1);

        return result.toArray(new String[result.size()]);
    }

    /**
     * Convert a path string to encoded path Strings in XPATH queries
     *
     * @param str Any string.
     * @return A valid string path suitable for use in XPATH queries
     */
    public static String stringToJCRPathExp(String str) {
        return ISO9075.encodePath(str);
    }

    /**
     * Convert a string to a JCR search expression literal, suitable for use in
     * jcr:contains() (inside XPath) or contains (SQL2). The characters - and " have
     * special meaning, and may be escaped with a backslash to obtain their
     * literal value. See JSR-283 spec v2.0, Sec. 6.7.19.
     *
     * @param str Any string.
     * @return A valid string literal suitable for use in
     * JCR contains clauses, including enclosing quotes.
     */
    public static String stringToJCRSearchExp(String str) {
        // escape single double quotes and \ except if preceded by a \

        if (str == null) {
            throw new IllegalArgumentException("Must pass a valid String");
        }

        str = str.trim();
        final int length = str.length();

        // quickly return
        if (length == 0) {
            return "''";
        }

        // if we don't have a double quote, just return the given string as a query literal
        int nextDoubleQuote = str.indexOf('"');
        if (nextDoubleQuote < 0) {
            return stringToQueryLiteral(str);
        }

        StringBuilder stringBuilder = new StringBuilder(length + 10);

        // copy string up to first double quote
        stringBuilder.append(str.substring(0, nextDoubleQuote));

        char previousChar = 0;
        boolean hasStartingDoubleQuote = false;
        for (int i = nextDoubleQuote; i < length; i++) {
            char c = str.charAt(i);
            if (c == '"') {
                // only check if we have another double quote later in which case we should not escape if we haven't seen one already
                if (!hasStartingDoubleQuote) {
                    nextDoubleQuote = str.indexOf('"', i + 1);
                    if (nextDoubleQuote < 0) {
                        // only escape if we don't have a preceding \ and we don't have a starting double quote
                        if (previousChar != '\\') {
                            stringBuilder.append('\\');
                        }
                        // and finish the string since we don't have anything left to escape
                        stringBuilder.append(str.substring(i, length));
                        break;
                    }
                    hasStartingDoubleQuote = true;
                } else {
                    hasStartingDoubleQuote = false;
                }
            }

            stringBuilder.append(c);
            previousChar = c;
        }
        return stringToQueryLiteral(stringBuilder.toString());
    }

    /**
     * Convert a string to a literal, suitable for inclusion
     * in a query. See JSR-283 spec v2.0, Sec. 4.6.6.19.
     *
     * @param str Any string.
     * @return A valid JCR query string literal, including enclosing quotes.
     */
    public static String stringToQueryLiteral(String str) {
        // Single quotes needed for jcr:contains()
        return "'" + Patterns.SINGLE_QUOTE.matcher(str).replaceAll("''") + "'";
    }

    /**
     * Decode an encoded JCR local name encoded with the {@link #escapeLocalNodeName(String)} method
     *
     * @param encodedLocalName the node name to unescape
     * @return the unescaped name
     */
    public static String unescapeLocalNodeName(final String encodedLocalName) {
        return encodedLocalName != null && encodedLocalName.indexOf('%') != -1
                ? Text.unescapeIllegalJcrChars(encodedLocalName)
                : encodedLocalName;
    }

    /**
     * Generates the node name by using a configurable NameGenerationHelper implementation.
     *
     * @return a node name generated by configurable helper service
     */
    public String generateNodeName(JCRNodeWrapper parent, String nodeType) {
        return getNameGenerationHelper().generatNodeName(parent, nodeType);
    }

    /**
     * Generates the node name by using a configurable NameGenerationHelper implementation.
     *
     * @return a node name generated by configurable helper service
     */
    public String generateNodeName(JCRNodeWrapper parent, String defaultLanguage, ExtendedNodeType nodeType,
            String targetName) {
        return getNameGenerationHelper().generatNodeName(parent, defaultLanguage, nodeType, targetName);
    }

    /**
     * Returns a mapping between file extensions and corresponding icons.
     *
     * @return a mapping between file extensions and corresponding icons
     */
    public Map<String, String> getFileExtensionIcons() {
        return fileExtensionIcons;
    }

    /**
     * Return a map of mime types (file formats) to be available in the
     * advanced search form.
     *
     * @return a map of mime types (file formats) to be available in the
     *         advanced search form
     */
    public Map<String, List<String>> getMimeTypes() {
        return mimeTypes;
    }

    public NameGenerationHelper getNameGenerationHelper() {
        return nameGenerationHelper;
    }

    public Set<String> getUnsupportedMarkForDeletionNodeTypes() {
        return unsupportedMarkForDeletionNodeTypes;
    }

    public void setNameGenerationHelper(NameGenerationHelper nameGenerationHelper) {
        this.nameGenerationHelper = nameGenerationHelper;
    }

    public void setUnsupportedMarkForDeletionNodeTypes(Set<String> unsupportedMarkForDeletionNodeTypes) {
        this.unsupportedMarkForDeletionNodeTypes = unsupportedMarkForDeletionNodeTypes;
    }

    public Pattern getHandleFallbackLocaleForPathPattern() {
        return handleFallbackLocaleForPath;
    }

    public void setHandleFallbackLocaleForPath(String handleFallbackLocaleForPath) {
        this.handleFallbackLocaleForPath = StringUtils.isNotEmpty(handleFallbackLocaleForPath)
                ? Pattern.compile(handleFallbackLocaleForPath)
                : null;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static List<Map<String, Object>> getRolesForNode(JCRNodeWrapper node, boolean includeInherited,
            boolean expandGroups, String roles, int limit, boolean latestFirst) {
        List<Map<String, Object>> results = new LinkedList<Map<String, Object>>();
        Map<String, List<String[]>> entries = node.getAclEntries();
        if (latestFirst) {
            entries = reverse(entries);
        }
        String siteKey = null;
        List<String> rolesList = Arrays.asList(StringUtils.splitByWholeSeparator(roles, null));
        JahiaUserManagerService userService = ServicesRegistry.getInstance().getJahiaUserManagerService();
        JahiaGroupManagerService groupService = ServicesRegistry.getInstance().getJahiaGroupManagerService();

        for (Map.Entry<String, List<String[]>> entry : entries.entrySet()) {
            Map<String, Object> m = new HashMap<String, Object>();
            String entryKey = entry.getKey();
            if (siteKey == null) {
                try {
                    JCRSiteNode resolveSite = node.getResolveSite();
                    siteKey = resolveSite != null ? resolveSite.getSiteKey() : null;
                } catch (RepositoryException e) {
                    logger.error(e.getMessage(), e);
                }
            }
            if (entryKey.startsWith("u:")) {
                String name = StringUtils.substringAfter(entryKey, "u:");
                JCRUserNode u = userService.lookupUser(name, siteKey);
                if (u == null) {
                    logger.warn("User {} cannot be found. Skipping.", name);
                    continue;
                }
                m.put("principalType", "user");
                m.put("principal", u);
            } else if (entryKey.startsWith("g:")) {
                String name = StringUtils.substringAfter(entryKey, "g:");
                JCRGroupNode g = groupService.lookupGroup(siteKey, name);
                if (g == null) {
                    g = groupService.lookupGroup(null, name);
                }
                if (g == null) {
                    logger.warn("Group {} cannot be found. Skipping.", name);
                    continue;
                }
                m.put("principalType", "group");
                m.put("principal", g);
            }

            for (String[] details : entry.getValue()) {
                if (details[1].equals("GRANT")) {
                    if (!rolesList.isEmpty()) {
                        if (!rolesList.contains(details[2])) {
                            continue;
                        }
                    }

                    if (!includeInherited) {
                        if (!details[0].equals(node.getPath())) {
                            continue;
                        }
                    }
                    if (!m.containsKey("roles")) {
                        m.put("roles", new LinkedList<String>());
                        results.add(m);
                    }
                    ((List) m.get("roles")).add(details[2]);
                }
            }

            if (limit > 0 && results.size() >= limit) {
                break;
            }
        }
        if (expandGroups) {
            List<Map<String, Object>> expandedResults = new LinkedList<Map<String, Object>>();
            for (Map<String, Object> result : results) {
                if (result.get("principalType").equals("group")) {
                    JCRGroupNode g = (JCRGroupNode) result.get("principal");
                    Set<JCRUserNode> principals = g.getRecursiveUserMembers();
                    for (JCRUserNode user : principals) {
                        Map<String, Object> m = new HashMap<String, Object>(result);
                        m.put("principalType", "user");
                        m.put("principal", user);
                        expandedResults.add(m);
                    }
                } else {
                    expandedResults.add(result);
                }
            }
            results = expandedResults;
        }

        return results;
    }

    public static <T> Map<String, T> reverse(Map<String, T> orderedMap) {
        if (orderedMap == null || orderedMap.isEmpty()) {
            return orderedMap;
        }
        LinkedHashMap<String, T> reversed = new LinkedHashMap<String, T>(orderedMap.size());
        ListIterator<String> li = new LinkedList<String>(orderedMap.keySet()).listIterator(orderedMap.size());
        while (li.hasPrevious()) {
            String key = li.previous();
            reversed.put(key, orderedMap.get(key));
        }
        return reversed;
    }

    /**
     * Encode a string to be used in a JCR SQL2 query by "escaping" the single quotes.
     *
     * @param s the string to be encoded
     * @return encoded string to be used in a JCR SQL2 query
     */
    public static String sqlEncode(String s) {
        return s != null && s.indexOf('\'') != -1 ? Patterns.SINGLE_QUOTE.matcher(s).replaceAll("''") : s;
    }

    /**
     * Returns the first parent of the specified node, which has the ACL inheritance broken. If not found, null<code>null</code> is
     * returned.
     *
     * @param node the node to search parent for
     * @return the first parent of the specified node, which has the ACL inheritance broken. If not found, null<code>null</code> is returned
     * @throws RepositoryException in case of JCR errors
     */
    public static JCRNodeWrapper getParentWithAclInheritanceBroken(JCRNodeWrapper node) throws RepositoryException {
        JCRNodeWrapper found = null;
        JCRNodeWrapper parent = node;
        try {
            while (true) {
                parent = parent.getParent();
                if (parent.getAclInheritanceBreak()) {
                    found = parent;
                    break;
                }
            }
        } catch (ItemNotFoundException e) {
            // reached the root node
        }

        return found;
    }

    /**
     * Detects the mime-type for the specified file name, based on its extension (uses mime types, configured in the web.xml deployment
     * descriptor).
     *
     * @param fileName the name of the file to detect mime type for
     * @return the mime-type for the specified file name, based on its extension (uses mime types, configured in the web.xml deployment
     *         descriptor)
     */
    public static String getMimeType(String fileName) {
        return fileName != null ? getInstance().servletContext.getMimeType(fileName.toLowerCase()) : null;
    }

    /**
     * Detects the mime-type for the specified file name, based on its extension (uses mime types, configured in the web.xml deployment
     * descriptor). If the type cannot be detected, the provided fallback mime type is returned.
     *
     * @param fileName         the name of the file to detect mime type for
     * @param fallbackMimeType the fallback mime-type to use if the type cannot be detected
     * @return the mime-type for the specified file name, based on its extension (uses mime types, configured in the web.xml deployment
     *         descriptor). If the type cannot be detected, the provided fallback mime type is returned.
     */
    public static String getMimeType(String fileName, String fallbackMimeType) {
        return StringUtils.defaultIfEmpty(getMimeType(fileName), fallbackMimeType);
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * Returns the /files/private folder for the current session user, creating it if it does not exist yet.
     *
     * @param session current JCR session
     * @return the JCR node, which corresponds to the /files/private folder for the current user
     * @throws RepositoryException in case of an error
     */
    public JCRNodeWrapper getUserPrivateFilesFolder(JCRSessionWrapper session) throws RepositoryException {
        JCRNodeWrapper privateFilesFolder = getDefaultUserFolder(session, "/files/private", false);
        if (privateFilesFolder.isNew()) {
            privateFilesFolder.grantRoles("u:" + session.getUser().getName(), Collections.singleton("owner"));
            privateFilesFolder.setAclInheritanceBreak(true);
            session.save();
        }

        return privateFilesFolder;
    }

    public JCRNodeWrapper getDefaultUserFolder(JCRSessionWrapper session, String path) throws RepositoryException {
        return getDefaultUserFolder(session, path, true);
    }

    /**
     * Retrieves the default user folder found at the specified path, creating it (and all intermediate folders) if needed, saving the session if requested.
     * @param session the session with which to access the JCR data
     * @param path the path of the default user folder to retrieve, if path is empty or <code>null</code> the user's node is returned
     * @param saveIfCreate <code>true</code> if we want the session to be immediately saved, <code>false</code> if the client code will save the session to commit the changes
     * @return the JCR node associated with the requested default user folder
     * @throws RepositoryException
     */
    public JCRNodeWrapper getDefaultUserFolder(JCRSessionWrapper session, String path, boolean saveIfCreate)
            throws RepositoryException {
        // if path is null or empty, return the user's node
        if (StringUtils.isEmpty(path)) {
            return session.getUserNode();
        }

        // make it possible to use relative paths without '/' prefix
        if (!path.startsWith("/")) {
            path = "/" + path;
        }

        // first check that we know this default user folder
        final String primaryNodeTypeName = defaultUserFolderTypes.get(path);
        if (primaryNodeTypeName == null) {
            throw new IllegalArgumentException("Unknown default user folder: " + path
                    + ". Known default user folders are: " + defaultUserFolderTypes);
        }

        final String userPath = session.getUserNode().getPath();
        if (!session.itemExists(userPath + path)) {
            final String name = StringUtils.substringAfterLast(path, "/");
            final JCRNodeWrapper parentUserFolder = getDefaultUserFolder(session,
                    StringUtils.substringBeforeLast(path, "/"), false);
            final JCRNodeWrapper userFolder = parentUserFolder.addNode(name, primaryNodeTypeName);

            if (saveIfCreate) {
                session.save();
            }

            return userFolder;
        }

        return session.getNode(userPath + path);
    }
}