/*
 * 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.jackrabbit.core.security.user;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.jcr.ImportUUIDBehavior;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.nodetype.ConstraintViolationException;

import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.Impersonation;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.commons.flat.PropertySequence;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.session.SessionWriteOperation;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.util.ReferenceChangeTracker;
import org.apache.jackrabbit.core.xml.NodeInfo;
import org.apache.jackrabbit.core.xml.PropInfo;
import org.apache.jackrabbit.core.xml.ProtectedNodeImporter;
import org.apache.jackrabbit.core.xml.ProtectedPropertyImporter;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>UserImporter</code> implements a <code>DefaultProtectedPropertyImporter</code>
 * and <code>DefaultProtectedNodeImporter</code> that is able to deal with
 * user/group content as defined by the default user related node types present
 * with jackrabbit-core.<p/>
 *
 * The importer is intended to be used by applications that import user content
 * extracted from another repository instance and immediately persist the
 * imported content using {@link javax.jcr.Session#save()}. Omitting the
 * save call will lead to transient, semi-validated user content and eventually
 * to inconsistencies.
 * <p>
 * Note the following restrictions:
 * <ul>
 * <li>The importer will only be initialized if the user manager is an instance
 * of <code>UserPerWorkspaceUserManager</code>.
 * </li>
 * <li>The importer will only be initialized if the editing session starting
 * this import is the same as the UserManager's Session instance.
 * </li>
 * <li>The jcr:uuid property of user and groups is defined to represent the
 * hashed authorizable id as calculated by the UserManager. This importer
 * is therefore not able to handle imports with
 * {@link ImportUUIDBehavior#IMPORT_UUID_CREATE_NEW}.</li>
 * <li>Importing user/group nodes outside of the hierarchy defined by
 * {@link org.apache.jackrabbit.core.security.user.UserManagerImpl#getUsersPath()}
 * and {@link org.apache.jackrabbit.core.security.user.UserManagerImpl#getGroupsPath()}
 * will fail upon save as the mandatory properties will not be imported. The same may
 * be true in case of {@link ImportUUIDBehavior#IMPORT_UUID_COLLISION_REPLACE_EXISTING}
 * inserting the user/group node at some other place in the node hierarchy.</li>
 * <li>While creating user/groups through the API the <code>UserManagerImpl</code> makes
 * sure that authorizables are never nested and are created below a hierarchy
 * of nt:AuthorizableFolder nodes. This isn't enforced by means of node type
 * constraints but only by the API. This importer currently doesn't perform such
 * a validation check.</li>
 * <li>Any attempt to import conflicting data will cause the import to fail
 * either immediately or upon calling {@link javax.jcr.Session#save()} with the
 * following exceptions:
 * <ul>
 * <li><code>rep:members</code> : Group membership</li>
 * <li><code>rep:impersonators</code> : Impersonators of a User.</li>
 * </ul>
 * The import behavior of these two properties is defined by the {@link #PARAM_IMPORT_BEHAVIOR}
 * configuration parameter, which can be set to
 * <ul>
 * <li>{@link ImportBehavior#NAME_IGNORE ignore}: A warning is logged.</li>
 * <li>{@link ImportBehavior#NAME_BESTEFFORT best effort}: A warning is logged
 * and the importer tries to fix the problem.</li>
 * <li>{@link ImportBehavior#NAME_ABORT abort}: The import is immediately
 * aborted with a ConstraintViolationException. (<strong>default</strong>)</li>
 * </ul>
 * </li>
 * </ul>
 * Known Issue:<br>
 * Importing <code>rep:impersonators</code> property referring to principals
 * that are created during this import AND have principalName different from the
 * ID will no succeed, as the validation in <code>ImpersonationImpl</code> isn't able
 * to find the authorizable with the given principal (reason: query will only
 * find persisted content).
 */
public class UserImporter implements ProtectedPropertyImporter, ProtectedNodeImporter {

    private static final Logger log = LoggerFactory.getLogger(UserImporter.class);

    /**
     * Parameter name for the import behavior configuration option.
     */
    public static final String PARAM_IMPORT_BEHAVIOR = "importBehavior";

    private JackrabbitSession session;

    private NamePathResolver resolver;

    private ReferenceChangeTracker referenceTracker;

    private UserPerWorkspaceUserManager userManager;

    private boolean initialized = false;

    private boolean resetAutoSave = false;

    private int importBehavior = ImportBehavior.IGNORE;

    /**
     * Container used to collect group members stored in protected nodes.
     */
    private Membership currentMembership;

    /**
     * Temporary store for the pw an imported new user to be able to call
     * the creation actions irrespective of the order of protected properties
     */
    private Map<String,String> currentPw = new HashMap<String,String>(1);

    public boolean init(JackrabbitSession session, NamePathResolver resolver,
                        boolean isWorkspaceImport,
                        int uuidBehavior, ReferenceChangeTracker referenceTracker) {

        this.session = session;
        this.resolver = resolver;
        this.referenceTracker = referenceTracker;

        if (initialized) {
            throw new IllegalStateException("Already initialized");
        }
        if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) {
            log.debug("ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW isn't supported when importing users or groups.");
            return false;
        }
        if (isWorkspaceImport) {
            log.debug("Only Session-Import is supported when importing users or groups.");
            return false;
        }
        try {
            UserManager uMgr = session.getUserManager();
            if (uMgr instanceof UserPerWorkspaceUserManager) {
                // make sure the user managers autosave flag can be changed to false.
                if (uMgr.isAutoSave()) {
                    uMgr.autoSave(false);
                    resetAutoSave = true;
                    log.debug("Changed autosave behavior of UserManager to 'false'.");
                }
                userManager = (UserPerWorkspaceUserManager) uMgr;
                initialized = true;
            } else {
                // either wrong implementation or one that implicitly calls save.
                log.debug("Failed to initialize UserImporter: UserManager isn't instance of UserPerWorkspaceUserManager or does implicit save call.");
            }
        } catch (RepositoryException e) {
            // failed to access user manager or to set the autosave behavior
            // -> return false (not initialized) as importer can't operate.
            log.error("Failed to initialize UserImporter: ", e);
        }
        return initialized;
    }

    // -----------------------------------------< ProtectedPropertyImporter >---
    /**
     * @see ProtectedPropertyImporter#handlePropInfo(org.apache.jackrabbit.core.NodeImpl, org.apache.jackrabbit.core.xml.PropInfo, org.apache.jackrabbit.spi.QPropertyDefinition)
     */
    public boolean handlePropInfo(NodeImpl parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException {
        if (!initialized) {
            throw new IllegalStateException("Not initialized");
        }

        /* importer can only handle protected properties below user/group
           nodes that are properly stored underneath the configured users/groups
           hierarchies (see {@link UserManagerImpl#getAuthorizable(NodeImpl)}.
           this prevents from importing user/group nodes somewhere in the
           content hierarchy which isn't possible when creating user/groups
           using the corresponding API calls  {@link UserManager#createUser} or
           {@link UserManager#createGroup} respectively. */
        Authorizable a = userManager.getAuthorizable(parent);
        if (a == null) {
            log.warn("Cannot handle protected PropInfo " + protectedPropInfo + ". Node " + parent + " doesn't represent a valid Authorizable.");
            return false;
        }

        // TODO: check if import should be aborted in case of nested authorizable.

        // assert that user manager is isn't in auto-save mode
        if (userManager.isAutoSave()) {
            userManager.autoSave(false);
        }
        try {
            Name propName = protectedPropInfo.getName();
            if (UserConstants.P_PRINCIPAL_NAME.equals(propName)) {
                // minimal validation that passed definition really matches the
                // protected rep:principalName property defined by rep:Authorizable.
                if (def.isMultiple() || !UserConstants.NT_REP_AUTHORIZABLE.equals(def.getDeclaringNodeType())) {
                    // some other unexpected property definition -> cannot handle
                    log.warn("Unexpected definition for property rep:principalName");
                    return false;
                }

                Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0];
                String princName = v.getString();
                userManager.setPrincipal(parent, new PrincipalImpl(princName));

                /*
                Execute authorizable actions for a NEW group as this is the
                same place in the userManager#createGroup that the actions
                are called.
                In case of a NEW user the actions are executed if the password
                has been imported before.
                */
                if (parent.isNew()) {
                    if (a.isGroup()) {
                        userManager.onCreate((Group) a);
                    } else if (currentPw.containsKey(a.getID())) {
                        userManager.onCreate((User) a, currentPw.remove(a.getID()));
                    }
                }

                return true;
            } else if (UserConstants.P_PASSWORD.equals(propName)) {
                if (a.isGroup()) {
                    log.warn("Expected parent node of type rep:User.");
                    return false;
                }
                // minimal validation of the passed definition
                if (def.isMultiple() || !UserConstants.NT_REP_USER.equals(def.getDeclaringNodeType())) {
                    // some other unexpected property definition -> cannot handle
                    log.warn("Unexpected definition for property rep:password");
                    return false;
                }

                Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0];
                String pw = v.getString();
                userManager.setPassword(parent, pw, false);

                /*
                 Execute authorizable actions for a NEW user at this point after
                 having set the password if the principal name has already been
                 processed, otherwise postpone it.
                 */
                if (parent.isNew()) {
                    if (parent.hasProperty(UserConstants.P_PRINCIPAL_NAME)) {
                        userManager.onCreate((User) a, pw);
                    } else {
                        // principal name not yet available -> remember the pw
                        currentPw.clear();
                        currentPw.put(a.getID(), pw);
                    }
                }
                return true;

            } else if (UserConstants.P_IMPERSONATORS.equals(propName)) {
                if (a.isGroup()) {
                    // unexpected parent type -> cannot handle
                    log.warn("Expected parent node of type rep:User.");
                    return false;
                }

                // minimal validation of the passed definition
                if (!def.isMultiple() || !UserConstants.MIX_REP_IMPERSONATABLE.equals(def.getDeclaringNodeType())) {
                    // some other unexpected property definition -> cannot handle
                    log.warn("Unexpected definition for property rep:impersonators");
                    return false;
                }

                // since impersonators may be imported later on, postpone processing
                // to the end.
                // see -> process References
                Value[] vs = protectedPropInfo.getValues(PropertyType.STRING, resolver);
                referenceTracker.processedReference(new Impersonators(a.getID(), vs));
                return true;

            } else if (UserConstants.P_DISABLED.equals(propName)) {
                if (a.isGroup()) {
                    log.warn("Expected parent node of type rep:User.");
                    return false;
                }
                // minimal validation of the passed definition
                if (def.isMultiple() || !UserConstants.NT_REP_USER.equals(def.getDeclaringNodeType())) {
                    // some other unexpected property definition -> cannot handle
                    log.warn("Unexpected definition for property rep:disabled");
                    return false;
                }

                Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0];
                ((User) a).disable(v.getString());

                return true;

            } else if (UserConstants.P_MEMBERS.equals(propName)) {
                if (!a.isGroup()) {
                    // unexpected parent type -> cannot handle
                    log.warn("Expected parent node of type rep:Group.");
                    return false;
                }

                // minimal validation of the passed definition
                if (!def.isMultiple() || !UserConstants.NT_REP_GROUP.equals(def.getDeclaringNodeType())) {
                    // some other unexpected property definition -> cannot handle
                    log.warn("Unexpected definition for property rep:members");
                    return false;
                }

                // since group-members are references to user/groups that potentially
                // are to be imported later on -> postpone processing to the end.
                // see -> process References
                Membership membership = new Membership(a.getID());
                for (Value v : protectedPropInfo.getValues(PropertyType.WEAKREFERENCE, resolver)) {
                    membership.addMember(new NodeId(v.getString()));
                }
                referenceTracker.processedReference(membership);
                return true;

            } // else: cannot handle -> return false

            return false;
        } finally {
            // reset the autosave mode of the user manager in order to restore
            // the original state.
            if (resetAutoSave) {
                userManager.autoSave(true);
            }
        }
    }

    /**
     * @see ProtectedPropertyImporter#handlePropInfo(org.apache.jackrabbit.core.NodeImpl, org.apache.jackrabbit.core.xml.PropInfo, org.apache.jackrabbit.spi.QPropertyDefinition)
     */
    public boolean handlePropInfo(NodeState parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException {
        return false;
    }

    /**
     * @see org.apache.jackrabbit.core.xml.ProtectedPropertyImporter#processReferences()
     */
    public void processReferences() throws RepositoryException {
        if (!initialized) {
            throw new IllegalStateException("Not initialized");
        }

        // assert that user manager is isn't in auto-save mode
        if (userManager.isAutoSave()) {
            userManager.autoSave(false);
        }
        try {
            List<Object> processed = new ArrayList<Object>();
            for (Iterator<Object> it = referenceTracker.getProcessedReferences(); it.hasNext();) {
                Object reference = it.next();
                if (reference instanceof Membership) {
                    Authorizable a = userManager.getAuthorizable(((Membership) reference).groupId);
                    if (a == null || !a.isGroup()) {
                        throw new RepositoryException(((Membership) reference).groupId + " does not represent a valid group.");
                    }

                    final Group gr = (Group) a;
                    // 1. collect members to add and to remove.
                    Map<String, Authorizable> toRemove = new HashMap<String, Authorizable>();
                    for (Iterator<Authorizable> declMembers = gr.getDeclaredMembers(); declMembers.hasNext();) {
                        Authorizable dm = declMembers.next();
                        toRemove.put(dm.getID(), dm);
                    }

                    List<Authorizable> toAdd = new ArrayList<Authorizable>();
                    final List<Membership.Member> nonExisting = new ArrayList<Membership.Member>();

                    for (Membership.Member member : ((Membership) reference).members) {
                        NodeId remapped = referenceTracker.getMappedId(member.id);
                        NodeId id = (remapped == null) ? member.id : remapped;

                        Authorizable authorz = null;
                        try {
                            NodeImpl n = ((SessionImpl) session).getNodeById(id);
                            authorz = userManager.getAuthorizable(n);
                        } catch (RepositoryException e) {
                            // no such node or failed to retrieve authorizable
                            // warning is logged below.
                        }
                        if (authorz != null) {
                            if (toRemove.remove(authorz.getID()) == null) {
                                toAdd.add(authorz);
                            } // else: no need to remove from rep:members
                        } else {
                            handleFailure("New member of " + gr + ": No such authorizable (NodeID = " + id + ")");
                            if (importBehavior == ImportBehavior.BESTEFFORT) {
                                log.info("ImportBehavior.BESTEFFORT: Remember non-existing member for processing.");
                                nonExisting.add(member);
                            }
                        }
                    }

                    // 2. adjust members of the group
                    for (Authorizable m : toRemove.values()) {
                        if (!gr.removeMember(m)) {
                            handleFailure("Failed remove existing member (" + m + ") from " + gr);
                        }
                    }
                    for (Authorizable m : toAdd) {
                        if (!gr.addMember(m)) {
                            handleFailure("Failed add member (" + m + ") to " + gr);
                        }
                    }

                    // handling non-existing members in case of best-effort
                    if (!nonExisting.isEmpty()) {
                        log.info("ImportBehavior.BESTEFFORT: Found " + nonExisting.size() + " entries of rep:members pointing to non-existing authorizables. Adding to rep:members.");
                        final NodeImpl groupNode = ((AuthorizableImpl) gr).getNode();

                        if (userManager.hasMemberSplitSize()) {
                            userManager.performProtectedOperation((SessionImpl) session, new SessionWriteOperation<Object>() {
                                public Boolean perform(SessionContext context) throws RepositoryException {
                                    NodeImpl nMembers = (groupNode.hasNode(UserConstants.N_MEMBERS)
                                            ? groupNode.getNode(UserConstants.N_MEMBERS)
                                            : groupNode.addNode(UserConstants.N_MEMBERS, UserConstants.NT_REP_MEMBERS, null));

                                    // Create N_MEMBERS node structure for holding member references
                                    for (Membership.Member member : nonExisting) {
                                        PropertySequence properties = GroupImpl.getPropertySequence(nMembers, userManager);
                                        String propName = member.name;
                                        if (propName == null) {
                                            log.debug("Ignoring unnamed user with id {}", member.id);
                                            continue;
                                        }
                                        if (properties.hasItem(propName)) {
                                            log.debug("Overwriting authorizable {} which is already member of {}.", propName, gr);
                                            properties.removeProperty(propName);
                                        }
                                        Value newMember = session.getValueFactory().createValue(member.id.toString(), PropertyType.WEAKREFERENCE);
                                        properties.addProperty(propName, newMember);
                                    }
                                    return null;
                                }
                            });
                        } else {
                            // Create P_MEMBERS for holding member references
                            // build list of valid members set before ....
                            List<Value> memberValues = new ArrayList<Value>();
                            if (groupNode.hasProperty(UserConstants.P_MEMBERS)) {
                                Value[] vls = groupNode.getProperty(UserConstants.P_MEMBERS).getValues();
                                memberValues.addAll(Arrays.asList(vls));
                            }

                            // ... and the non-Existing ones.
                            for (Membership.Member member : nonExisting) {
                                memberValues.add(session.getValueFactory().createValue(member.id.toString(), PropertyType.WEAKREFERENCE));
                            }

                            // and use implementation specific method to set the
                            // value of rep:members properties which was not possible
                            // through the API
                            userManager.setProtectedProperty(groupNode,
                                    UserConstants.P_MEMBERS,
                                    memberValues.toArray(new Value[memberValues.size()]),
                                    PropertyType.WEAKREFERENCE);
                        }
                    }

                    processed.add(reference);

                } else if (reference instanceof Impersonators) {
                    Authorizable a = userManager.getAuthorizable(((Impersonators) reference).userId);
                    if (a == null || a.isGroup()) {
                        throw new RepositoryException(((Impersonators) reference).userId + " does not represent a valid user.");
                    }

                    Impersonation imp = ((User) a).getImpersonation();

                    // 1. collect principals to add and to remove.
                    Map<String, Principal> toRemove = new HashMap<String, Principal>();
                    for (PrincipalIterator pit = imp.getImpersonators(); pit.hasNext();) {
                        Principal princ = pit.nextPrincipal();
                        toRemove.put(princ.getName(), princ);
                    }

                    List<Principal> toAdd = new ArrayList<Principal>();
                    Value[] vs = ((Impersonators) reference).values;
                    for (Value v : vs) {
                        String princName = v.getString();
                        if (toRemove.remove(princName) == null) {
                            // add it to the list of new impersonators to be added.
                            toAdd.add(new PrincipalImpl(princName));
                        } // else: no need to revoke impersonation for the given principal.
                    }

                    // 2. adjust set of impersonators
                    for (Principal princ : toRemove.values()) {
                        if (!imp.revokeImpersonation(princ)) {
                            handleFailure("Failed to revoke impersonation for " + princ.getName() + " on " + a);
                        }
                    }
                    for (Principal princ : toAdd) {
                        if (!imp.grantImpersonation(princ)) {
                            handleFailure("Failed to grant impersonation for " + princ.getName() + " on " + a);
                        }
                    }
                    // NOTE: no best effort handling so far. (TODO)

                    processed.add(reference);
                }
            }
            // successfully processed this entry of the reference tracker
            // -> remove from the reference tracker.
            referenceTracker.removeReferences(processed);
        } finally {
            // reset the autosave mode of the user manager in order to restore
            // the original state.
            if (resetAutoSave) {
                userManager.autoSave(true);
            }
        }
    }

    // ---------------------------------------------< ProtectedNodeImporter >---
    /**
     * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.NodeImpl)
     */
    public boolean start(NodeImpl protectedParent) throws RepositoryException {
        String repMembers = resolver.getJCRName(UserConstants.NT_REP_MEMBERS);
        if (repMembers.equals(protectedParent.getPrimaryNodeType().getName())) {
            NodeImpl groupNode = protectedParent;
            while (groupNode.getDepth() != 0 &&
                    repMembers.equals(groupNode.getPrimaryNodeType().getName())) {

                groupNode = (NodeImpl) groupNode.getParent();
            }
            Authorizable auth = userManager.getAuthorizable(groupNode);
            if (auth == null) {
                log.debug("Cannot handle protected node " + protectedParent + ". It nor one of its parents represent a valid Authorizable.");
                return false;
            } else {
                currentMembership = new Membership(auth.getID());
                return true;
            }
        } else {
            return false;
        }
    }

    /**
     * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.state.NodeState)
     */
    public boolean start(NodeState protectedParent) {
        return false;
    }

    /**
     * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.NodeImpl)
     */
    public void startChildInfo(NodeInfo childInfo, List<PropInfo> propInfos) throws RepositoryException {
        assert (currentMembership != null);

        if (UserConstants.NT_REP_MEMBERS.equals(childInfo.getNodeTypeName())) {
            for (PropInfo prop : propInfos) {
                for (Value v : prop.getValues(PropertyType.WEAKREFERENCE, resolver)) {
                    String name = resolver.getJCRName(prop.getName());
                    NodeId id = new NodeId(v.getString());
                    currentMembership.addMember(name, id);
                }
            }
        } else {
            log.warn("{} is not of type {}", childInfo.getName(), UserConstants.NT_REP_MEMBERS);
        }
    }

    /**
     * @see org.apache.jackrabbit.core.xml.ProtectedNodeImporter#endChildInfo()
     */
    public void endChildInfo() throws RepositoryException {
    }

    /**
     * @see ProtectedNodeImporter#end(org.apache.jackrabbit.core.NodeImpl)
     */
    public void end(NodeImpl protectedParent) throws RepositoryException {
        referenceTracker.processedReference(currentMembership);
        currentMembership = null;
    }

    /**
     * @see ProtectedNodeImporter#end(org.apache.jackrabbit.core.state.NodeState)
     */
    public void end(NodeState protectedParent) {
    }

    //---------------------------------------------------------< BeanConfig >---
    /**
     * @return human readable representation of the <code>importBehavior</code> value.
     */
    public String getImportBehavior() {
        return ImportBehavior.nameFromValue(importBehavior);
    }

    /**
     *
     * @param importBehaviorStr
     */
    public void setImportBehavior(String importBehaviorStr) {
        this.importBehavior = ImportBehavior.valueFromName(importBehaviorStr);
    }

    //------------------------------------------------------------< private >---
    /**
     * Handling the import behavior
     *
     * @param msg
     * @throws RepositoryException
     */
    private void handleFailure(String msg) throws RepositoryException {
        switch (importBehavior) {
            case ImportBehavior.IGNORE:
            case ImportBehavior.BESTEFFORT:
                log.warn(msg);
                break;
            case ImportBehavior.ABORT:
                throw new ConstraintViolationException(msg);
            default:
                // no other behavior. nothing to do.

        }
    }

    //------------------------------------------------------< inner classes >---
    /**
     * Inner class used to postpone import of group membership to the very end
     * of the import. This allows to import membership of user/groups that
     * are only being created during this import.
     *
     * @see ImportBehavior For additional configuration options.
     */
    private static final class Membership {
        private final String groupId;
        private final List<Member> members = new LinkedList<Member>();

        public Membership(String groupId) {
            this.groupId = groupId;
        }

        public void addMember(String name, NodeId id) {
            members.add(new Member(name, id));
        }

        public void addMember(NodeId id) {
            addMember(null, id);
        }

        // If only Java had tuples...
        public class Member {
            private final String name;
            private final NodeId id;

            public Member(String name, NodeId id) {
                super();
                this.name = name;
                this.id = id;
            }
        }
    }

    /**
     * Inner class used to postpone import of impersonators to the very end
     * of the import. This allows to import impersonation values pointing
     * to user that are only being created during this import.
     *
     * @see ImportBehavior For additional configuration options.
     */
    private static final class Impersonators {

        private final String userId;
        private final Value[] values;

        private Impersonators(String userId, Value[] values) {
            this.userId = userId;
            this.values = values;
        }
    }

    /**
     * Inner class defining the treatment of membership or impersonator
     * values pointing to non-existing authorizables.
     */
    public static final class ImportBehavior {

        /**
         * If a member or impersonator value cannot be set due to constraints
         * enforced by the API implementation, the failure is logged as
         * warning but otherwise ignored.
         */
        public static final int IGNORE = 1;
        /**
         * Same as {@link #IGNORE} but in addition tries to circumvent the
         * problem. This option should only be used with validated and trusted
         * XML passed to the SessionImporter.
         */
        public static final int BESTEFFORT = 2;
        /**
         * Aborts the import as soon as invalid values are detected throwing
         * a <code>ConstraintViolationException</code>.
         */
        public static final int ABORT = 3;

        public static final String NAME_IGNORE = "ignore";
        public static final String NAME_BESTEFFORT = "besteffort";
        public static final String NAME_ABORT = "abort";

        public static int valueFromName(String behaviorString) {
            if (NAME_IGNORE.equalsIgnoreCase(behaviorString)) {
                return IGNORE;
            } else if (NAME_BESTEFFORT.equalsIgnoreCase(behaviorString)) {
                return BESTEFFORT;
            } else if (NAME_ABORT.equalsIgnoreCase(behaviorString)) {
                return ABORT;
            } else {
                log.error("Invalid behavior " + behaviorString + " -> Using default: ABORT.");
                return ABORT;
            }
        }

        public static String nameFromValue(int importBehavior) {
            switch (importBehavior) {
                case ImportBehavior.IGNORE:
                    return NAME_IGNORE;
                case ImportBehavior.ABORT:
                    return NAME_ABORT;
                case ImportBehavior.BESTEFFORT:
                    return NAME_BESTEFFORT;
                default:
                    throw new IllegalArgumentException("Invalid import behavior: " + importBehavior);
            }
        }
    }
}