org.pentaho.di.repository.pur.PurRepository.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.di.repository.pur.PurRepository.java

Source

//CHECKSTYLE:FileLength:OFF
/*!
* Copyright 2010 - 2017 Pentaho Corporation.  All rights reserved.
*
* Licensed 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.pentaho.di.repository.pur;

import java.io.Serializable;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.ws.Service;

import org.apache.commons.lang.StringUtils;
import org.pentaho.di.cluster.ClusterSchema;
import org.pentaho.di.cluster.SlaveServer;
import org.pentaho.di.core.Condition;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.ProgressMonitorListener;
import org.pentaho.di.core.annotations.RepositoryPlugin;
import org.pentaho.di.core.changed.ChangedFlagInterface;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.exception.IdNotFoundException;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettleFileException;
import org.pentaho.di.core.exception.KettleSecurityException;
import org.pentaho.di.core.extension.ExtensionPointHandler;
import org.pentaho.di.core.extension.KettleExtensionPoint;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.job.JobMeta;
import org.pentaho.di.partition.PartitionSchema;
import org.pentaho.di.repository.AbstractRepository;
import org.pentaho.di.repository.IRepositoryExporter;
import org.pentaho.di.repository.IRepositoryImporter;
import org.pentaho.di.repository.IRepositoryService;
import org.pentaho.di.repository.IUser;
import org.pentaho.di.repository.ObjectId;
import org.pentaho.di.repository.ObjectRevision;
import org.pentaho.di.repository.ReconnectableRepository;
import org.pentaho.di.repository.Repository;
import org.pentaho.di.repository.RepositoryDirectory;
import org.pentaho.di.repository.RepositoryDirectoryInterface;
import org.pentaho.di.repository.RepositoryElementInterface;
import org.pentaho.di.repository.RepositoryElementMetaInterface;
import org.pentaho.di.repository.RepositoryExtended;
import org.pentaho.di.repository.RepositoryMeta;
import org.pentaho.di.repository.RepositoryObject;
import org.pentaho.di.repository.RepositoryObjectType;
import org.pentaho.di.repository.RepositorySecurityManager;
import org.pentaho.di.repository.RepositorySecurityProvider;
import org.pentaho.di.repository.StringObjectId;
import org.pentaho.di.repository.pur.metastore.PurRepositoryMetaStore;
import org.pentaho.di.repository.pur.model.EEJobMeta;
import org.pentaho.di.repository.pur.model.EERepositoryObject;
import org.pentaho.di.repository.pur.model.EETransMeta;
import org.pentaho.di.repository.pur.model.EEUserInfo;
import org.pentaho.di.repository.pur.model.RepositoryLock;
import org.pentaho.di.shared.SharedObjectInterface;
import org.pentaho.di.shared.SharedObjects;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.ui.repository.pur.services.IAbsSecurityProvider;
import org.pentaho.di.ui.repository.pur.services.IAclService;
import org.pentaho.di.ui.repository.pur.services.ILockService;
import org.pentaho.di.ui.repository.pur.services.IRevisionService;
import org.pentaho.metastore.api.IMetaStore;
import org.pentaho.metastore.api.exceptions.MetaStoreException;
import org.pentaho.metastore.api.exceptions.MetaStoreNamespaceExistsException;
import org.pentaho.metastore.util.PentahoDefaults;
import org.pentaho.platform.api.repository2.unified.IUnifiedRepository;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl;
import org.pentaho.platform.api.repository2.unified.RepositoryFileTree;
import org.pentaho.platform.api.repository2.unified.RepositoryRequest;
import org.pentaho.platform.api.repository2.unified.VersionSummary;
import org.pentaho.platform.api.repository2.unified.RepositoryRequest.FILES_TYPE_FILTER;
import org.pentaho.platform.api.repository2.unified.data.node.DataNode;
import org.pentaho.platform.api.repository2.unified.data.node.NodeRepositoryFileData;
import org.pentaho.platform.repository.RepositoryFilenameUtils;
import org.pentaho.platform.repository2.ClientRepositoryPaths;
import org.pentaho.platform.repository2.unified.webservices.jaxws.IUnifiedRepositoryJaxwsWebService;

/**
 * Implementation of {@link Repository} that delegates to the Pentaho unified repository (PUR), an instance of
 * {@link IUnifiedRepository}.
 *
 * @author Matt
 * @author mlowery
 */
@SuppressWarnings("deprecation")
@RepositoryPlugin(id = "PentahoEnterpriseRepository", name = "RepositoryType.Name.EnterpriseRepository", description = "RepositoryType.Description.EnterpriseRepository", metaClass = "org.pentaho.di.repository.pur.PurRepositoryMeta", i18nPackageName = "org.pentaho.di.repository.pur")
public class PurRepository extends AbstractRepository
        implements Repository, ReconnectableRepository, RepositoryExtended, java.io.Serializable {

    private static final long serialVersionUID = 7460109109707189479L; /* EESOURCE: UPDATE SERIALVERUID */

    // Kettle property that when set to false disabled the lazy repository access
    public static final String LAZY_REPOSITORY = "KETTLE_LAZY_REPOSITORY";

    private static Class<?> PKG = PurRepository.class;

    // ~ Static fields/initializers ======================================================================================

    // private static final Log logger = LogFactory.getLog(PurRepository.class);

    private static final String REPOSITORY_VERSION = "1.0"; //$NON-NLS-1$

    private static final boolean VERSION_SHARED_OBJECTS = true;

    private static final String FOLDER_PDI = "pdi"; //$NON-NLS-1$

    private static final String FOLDER_PARTITION_SCHEMAS = "partitionSchemas"; //$NON-NLS-1$

    private static final String FOLDER_CLUSTER_SCHEMAS = "clusterSchemas"; //$NON-NLS-1$

    private static final String FOLDER_SLAVE_SERVERS = "slaveServers"; //$NON-NLS-1$

    private static final String FOLDER_DATABASES = "databases"; //$NON-NLS-1$

    // ~ Instance fields =================================================================================================
    /**
     * Indicates that this code should be run in unit test mode (where PUR is passed in instead of created inside this
     * class).
     */
    private boolean test = false;

    private IUnifiedRepository pur;

    private IUser user;

    private PurRepositoryMeta repositoryMeta;

    private DatabaseDelegate databaseMetaTransformer = new DatabaseDelegate(this);

    private PartitionDelegate partitionSchemaTransformer = new PartitionDelegate(this);

    private SlaveDelegate slaveTransformer = new SlaveDelegate(this);

    private ClusterDelegate clusterTransformer = new ClusterDelegate(this);

    private ISharedObjectsTransformer transDelegate;

    private ISharedObjectsTransformer jobDelegate;

    private Map<RepositoryObjectType, SharedObjectAssembler<?>> sharedObjectAssemblerMap;

    private RepositorySecurityManager securityManager;

    private RepositorySecurityProvider securityProvider;

    protected LogChannelInterface log;

    protected Serializable cachedSlaveServerParentFolderId;

    protected Serializable cachedPartitionSchemaParentFolderId;

    protected Serializable cachedClusterSchemaParentFolderId;

    protected Serializable cachedDatabaseMetaParentFolderId;

    private final RootRef rootRef = new RootRef();

    private UnifiedRepositoryLockService unifiedRepositoryLockService;

    private Map<RepositoryObjectType, List<? extends SharedObjectInterface>> sharedObjectsByType = null;

    private boolean connected = false;

    private String connectMessage = null;

    protected PurRepositoryMetaStore metaStore;

    // The servers (DI Server, BA Server) that a user can authenticate to
    protected enum RepositoryServers {
        DIS, POBS
    }

    private IRepositoryConnector purRepositoryConnector;

    private RepositoryServiceRegistry purRepositoryServiceRegistry = new RepositoryServiceRegistry();

    // ~ Constructors ====================================================================================================

    public PurRepository() {
        super();
        initSharedObjectAssemblerMap();
    }

    // ~ Methods =========================================================================================================

    protected RepositoryDirectoryInterface getRootDir() throws KettleException {
        RepositoryDirectoryInterface ref = rootRef.getRef();
        return ref == null ? loadRepositoryDirectoryTree() : ref;
    }

    /**
     * public for unit tests.
     */
    public void setTest(final IUnifiedRepository pur) {
        this.pur = pur;
        // set this to avoid NPE in connect()
        this.repositoryMeta.setRepositoryLocation(new PurRepositoryLocation("doesnotmatch"));
        this.test = true;
    }

    private boolean isTest() {
        return test;
    }

    @Override
    public void init(final RepositoryMeta repositoryMeta) {
        this.log = new LogChannel(this.getClass().getSimpleName());
        this.repositoryMeta = (PurRepositoryMeta) repositoryMeta;
        purRepositoryConnector = new PurRepositoryConnector(this, this.repositoryMeta, rootRef);
    }

    public void setPurRepositoryConnector(IRepositoryConnector purRepositoryConnector) {
        this.purRepositoryConnector = purRepositoryConnector;
    }

    public RootRef getRootRef() {
        return rootRef;
    }

    @Override
    public void connect(final String username, final String password) throws KettleException {
        connected = false;
        if (isTest()) {
            connected = true;
            purRepositoryServiceRegistry.registerService(IRevisionService.class,
                    new UnifiedRepositoryRevisionService(pur, getRootRef()));
            purRepositoryServiceRegistry.registerService(ILockService.class, new UnifiedRepositoryLockService(pur));
            purRepositoryServiceRegistry.registerService(IAclService.class,
                    new UnifiedRepositoryConnectionAclService(pur));
            metaStore = new PurRepositoryMetaStore(this);
            try {
                metaStore.createNamespace(PentahoDefaults.NAMESPACE);
            } catch (MetaStoreException e) {
                log.logError(BaseMessages.getString(PKG, "PurRepositoryMetastore.NamespaceCreateException.Message",
                        PentahoDefaults.NAMESPACE), e);
            }
            this.user = new EEUserInfo(username, password, username, "test user", true);
            this.jobDelegate = new JobDelegate(this, pur);
            this.transDelegate = new TransDelegate(this, pur);
            this.unifiedRepositoryLockService = new UnifiedRepositoryLockService(pur);
            return;
        }
        try {
            if (log != null && purRepositoryConnector != null && purRepositoryConnector.getLog() != null) {
                purRepositoryConnector.getLog().setLogLevel(log.getLogLevel());
            }
            RepositoryConnectResult result = purRepositoryConnector.connect(username, password);
            this.user = result.getUser();
            this.connected = result.isSuccess();
            this.securityProvider = result.getSecurityProvider();
            this.securityManager = result.getSecurityManager();
            IUnifiedRepository r = result.getUnifiedRepository();
            try {
                this.pur = (IUnifiedRepository) Proxy.newProxyInstance(r.getClass().getClassLoader(),
                        new Class<?>[] { IUnifiedRepository.class },
                        new UnifiedRepositoryInvocationHandler<IUnifiedRepository>(r));
                if (this.securityProvider != null) {
                    this.securityProvider = (RepositorySecurityProvider) Proxy.newProxyInstance(
                            this.securityProvider.getClass().getClassLoader(),
                            new Class<?>[] { RepositorySecurityProvider.class },
                            new UnifiedRepositoryInvocationHandler<RepositorySecurityProvider>(
                                    this.securityProvider));
                }
            } catch (Throwable th) {
                if (log.isError()) {
                    log.logError("Failed to setup repository connection", th);
                }
                connected = false;
            }
            this.unifiedRepositoryLockService = new UnifiedRepositoryLockService(pur);
            this.connectMessage = result.getConnectMessage();
            this.purRepositoryServiceRegistry = result.repositoryServiceRegistry();
            this.transDelegate = new TransDelegate(this, pur);
            this.jobDelegate = new JobDelegate(this, pur);
        } finally {
            if (connected) {
                if (log.isBasic()) {
                    log.logBasic(BaseMessages.getString(PKG, "PurRepositoryMetastore.Create.Message"));
                }
                metaStore = new PurRepositoryMetaStore(this);
                // Create the default Pentaho namespace if it does not exist
                try {
                    metaStore.createNamespace(PentahoDefaults.NAMESPACE);
                    if (log.isBasic()) {
                        log.logBasic(
                                BaseMessages.getString(PKG, "PurRepositoryMetastore.NamespaceCreateSuccess.Message",
                                        PentahoDefaults.NAMESPACE));
                    }
                } catch (MetaStoreNamespaceExistsException e) {
                    // Ignore this exception, we only use it to save a call to check if the namespace exists, as the
                    // createNamespace()
                    // call will do the check for us and throw this exception.
                } catch (MetaStoreException e) {
                    log.logError(BaseMessages.getString(PKG,
                            "PurRepositoryMetastore.NamespaceCreateException.Message", PentahoDefaults.NAMESPACE),
                            e);
                }

                if (log.isBasic()) {
                    log.logBasic(BaseMessages.getString(PKG, "PurRepository.ConnectSuccess.Message"));
                }
            }
        }
    }

    @Override
    public boolean isConnected() {
        return connected;
    }

    @Override
    public void disconnect() {
        connected = false;
        metaStore = null;
        purRepositoryConnector.disconnect();
    }

    @Override
    public int countNrJobEntryAttributes(ObjectId idJobentry, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public int countNrStepAttributes(ObjectId idStep, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public RepositoryDirectoryInterface createRepositoryDirectory(
            final RepositoryDirectoryInterface parentDirectory, final String directoryPath) throws KettleException {
        try {
            RepositoryDirectoryInterface refreshedParentDir = findDirectory(parentDirectory.getPath());

            // update the passed in repository directory with the children recently loaded from the repo
            parentDirectory.setChildren(refreshedParentDir.getChildren());
            String[] path = Const.splitPath(directoryPath, RepositoryDirectory.DIRECTORY_SEPARATOR);

            RepositoryDirectoryInterface follow = parentDirectory;

            for (int level = 0; level < path.length; level++) {
                RepositoryDirectoryInterface child = follow.findChild(path[level]);
                if (child == null) {
                    // create this one
                    child = new RepositoryDirectory(follow, path[level]);
                    saveRepositoryDirectory(child);
                    // link this with the parent directory
                    follow.addSubdirectory(child);
                }

                follow = child;
            }
            return follow;
        } catch (Exception e) {
            throw new KettleException("Unable to create directory with path [" + directoryPath + "]", e);
        }
    }

    @Override
    public void saveRepositoryDirectory(final RepositoryDirectoryInterface dir) throws KettleException {
        try {
            // id of root dir is null--check for it
            if ("/".equals(dir.getParent().getName())) {
                throw new KettleException(
                        BaseMessages.getString(PKG, "PurRepository.FailedDirectoryCreation.Message"));
            }
            RepositoryFile newFolder = pur.createFolder(
                    dir.getParent().getObjectId() != null ? dir.getParent().getObjectId().getId() : null,
                    new RepositoryFile.Builder(dir.getName()).folder(true).build(), null);
            dir.setObjectId(new StringObjectId(newFolder.getId().toString()));
        } catch (Exception e) {
            throw new KettleException(
                    "Unable to save repository directory with path [" + getPath(null, dir, null) + "]", e);
        }
    }

    /**
     * Determine if "baseFolder" is the same as "folder" or if "folder" is a descendant of "baseFolder"
     *
     * @param folder
     *          Folder to test for similarity / ancestory; Must not be null
     * @param baseFolder
     *          Folder that may be the same or an ancestor; Must not be null
     * @return True if folder is a descendant of baseFolder or False if not; False if either folder or baseFolder are null
     */
    protected boolean isSameOrAncestorFolder(RepositoryFile folder, RepositoryFile baseFolder) {
        // If either folder is null, return false. We cannot do a proper comparison
        if (folder != null && baseFolder != null) {

            if (
            // If the folders are equal
            baseFolder.getId().equals(folder.getId()) || (
            // OR if the folders are NOT siblings AND the folder to move IS an ancestor to the users home folder
            baseFolder.getPath().lastIndexOf(RepositoryDirectory.DIRECTORY_SEPARATOR) != folder.getPath()
                    .lastIndexOf(RepositoryDirectory.DIRECTORY_SEPARATOR)
                    && baseFolder.getPath().startsWith(folder.getPath()))) {
                return true;
            }

        }
        return false;
    }

    /**
     * Test to see if the folder is a user's home directory If it is an ancestor to a user's home directory, false will be
     * returned. (It is not actually a user's home directory)
     *
     * @param folder
     *          The folder to test; Must not be null
     * @return True if the directory is a users home directory and False if it is not; False if folder is null
     */
    protected boolean isUserHomeDirectory(RepositoryFile folder) {
        if (folder != null) {

            // Get the root of all home folders
            RepositoryFile homeRootFolder = pur.getFile(ClientRepositoryPaths.getHomeFolderPath());
            if (homeRootFolder != null) {
                // Strip the final RepositoryDirectory.DIRECTORY_SEPARATOR from the paths
                String temp = homeRootFolder.getPath();
                String homeRootPath = temp.endsWith(RepositoryDirectory.DIRECTORY_SEPARATOR)
                        && temp.length() > RepositoryDirectory.DIRECTORY_SEPARATOR.length()
                                ? temp.substring(0,
                                        temp.length() - RepositoryDirectory.DIRECTORY_SEPARATOR.length())
                                : temp;
                temp = folder.getPath();
                String folderPath = temp.endsWith(RepositoryDirectory.DIRECTORY_SEPARATOR)
                        && temp.length() > RepositoryDirectory.DIRECTORY_SEPARATOR.length()
                                ? temp.substring(0,
                                        temp.length() - RepositoryDirectory.DIRECTORY_SEPARATOR.length())
                                : temp;

                // Is the folder in a user's home directory?
                if (folderPath.startsWith(homeRootPath)) {
                    if (folderPath.equals(homeRootPath)) {
                        return false;
                    }

                    // If there is exactly one more RepositoryDirectory.DIRECTORY_SEPARATOR in folderPath than homeRootFolder,
                    // then the user is trying to delete another user's home directory
                    int folderPathDirCount = 0;
                    int homeRootPathDirCount = 0;

                    for (int x = 0; x >= 0; folderPathDirCount++) {
                        x = folderPath.indexOf(RepositoryDirectory.DIRECTORY_SEPARATOR, x + 1);
                    }
                    for (int x = 0; x >= 0; homeRootPathDirCount++) {
                        x = homeRootPath.indexOf(RepositoryDirectory.DIRECTORY_SEPARATOR, x + 1);
                    }

                    if (folderPathDirCount == (homeRootPathDirCount + 1)) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    @Override
    public void deleteRepositoryDirectory(final RepositoryDirectoryInterface dir) throws KettleException {
        deleteRepositoryDirectory(dir, false);
    }

    @Override
    public void deleteRepositoryDirectory(final RepositoryDirectoryInterface dir,
            final boolean deleteHomeDirectories) throws KettleException {
        try {
            // Fetch the folder to be deleted
            RepositoryFile folder = pur.getFileById(dir.getObjectId().getId());

            // Fetch the user's home directory
            RepositoryFile homeFolder = pur.getFile(ClientRepositoryPaths.getUserHomeFolderPath(user.getLogin()));

            // Make sure the user is not trying to delete their own home directory
            if (isSameOrAncestorFolder(folder, homeFolder)) {
                // Then throw an exception that the user cannot delete their own home directory
                throw new KettleException("You are not allowed to delete your home folder.");
            }

            if (!deleteHomeDirectories && isUserHomeDirectory(folder)) {
                throw new RepositoryObjectAccessException("Cannot delete another users home directory",
                        RepositoryObjectAccessException.AccessExceptionType.USER_HOME_DIR);
            }

            pur.deleteFile(dir.getObjectId().getId(), null);
            rootRef.clearRef();
        } catch (Exception e) {
            throw new KettleException("Unable to delete directory with path [" + getPath(null, dir, null) + "]", e);
        }
    }

    @Override
    public ObjectId renameRepositoryDirectory(final ObjectId dirId, final RepositoryDirectoryInterface newParent,
            final String newName) throws KettleException {
        return renameRepositoryDirectory(dirId, newParent, newName, false);
    }

    @Override
    public ObjectId renameRepositoryDirectory(final ObjectId dirId, final RepositoryDirectoryInterface newParent,
            final String newName, final boolean renameHomeDirectories) throws KettleException {
        // dir ID is used to find orig obj; new parent is used as new parent (might be null meaning no change in parent);
        // new name is used as new file name (might be null meaning no change in name)
        String finalName = null;
        String finalParentPath = null;
        String interimFolderPath = null;
        try {
            RepositoryFile homeFolder = pur.getFile(ClientRepositoryPaths.getUserHomeFolderPath(user.getLogin()));
            RepositoryFile folder = pur.getFileById(dirId.getId());
            finalName = (newName != null ? newName : folder.getName());
            interimFolderPath = getParentPath(folder.getPath());
            finalParentPath = (newParent != null ? getPath(null, newParent, null) : interimFolderPath);
            // Make sure the user is not trying to move their own home directory
            if (isSameOrAncestorFolder(folder, homeFolder)) {
                // Then throw an exception that the user cannot move their own home directory
                throw new KettleException("You are not allowed to move/rename your home folder.");
            }

            if (!renameHomeDirectories && isUserHomeDirectory(folder)) {
                throw new RepositoryObjectAccessException("Cannot move another users home directory",
                        RepositoryObjectAccessException.AccessExceptionType.USER_HOME_DIR);
            }

            pur.moveFile(dirId.getId(), finalParentPath + RepositoryFile.SEPARATOR + finalName, null);
            rootRef.clearRef();
            return dirId;
        } catch (Exception e) {
            throw new KettleException("Unable to move/rename directory with id [" + dirId + "] to new parent ["
                    + finalParentPath + "] and new name [" + finalName + "]", e);
        }
    }

    protected RepositoryFileTree loadRepositoryFileTree(String path) {
        return pur.getTree(path, -1, null, true);
    }

    @Override
    public RepositoryDirectoryInterface loadRepositoryDirectoryTree(String path, String filter, int depth,
            boolean showHidden, boolean includeEmptyFolder, boolean includeAcls) throws KettleException {

        // First check for possibility of speedy algorithm
        if (filter == null && "/".equals(path) && includeEmptyFolder) {
            return initRepositoryDirectoryTree(loadRepositoryFileTreeFolders("/", -1, includeAcls, showHidden));
        }
        //load count levels from root to destination path to load folder tree
        int fromRootToDest = StringUtils.countMatches(path, "/");
        //create new root directory "/"
        RepositoryDirectory dir = new RepositoryDirectory();
        //fetch folder tree from root "/" to destination path for populate folder
        RepositoryFileTree rootDirTree = loadRepositoryFileTree("/", "*", fromRootToDest, showHidden, includeAcls,
                FILES_TYPE_FILTER.FOLDERS);
        //populate directory by folder tree
        fillRepositoryDirectoryFromTree(dir, rootDirTree);

        RepositoryDirectoryInterface destinationDir = dir.findDirectory(path);
        //search for goal path and filter
        RepositoryFileTree repoTree = loadRepositoryFileTree(path, filter, depth, showHidden, includeAcls,
                FILES_TYPE_FILTER.FILES_FOLDERS);
        //populate the directory with founded files and subdirectories with files
        fillRepositoryDirectoryFromTree(destinationDir, repoTree);

        if (includeEmptyFolder) {
            RepositoryDirectoryInterface folders = initRepositoryDirectoryTree(
                    loadRepositoryFileTree(path, null, depth, showHidden, includeAcls, FILES_TYPE_FILTER.FOLDERS));
            return copyFrom(folders, destinationDir);
        } else {
            return destinationDir;
        }
    }

    private RepositoryFileTree loadRepositoryFileTree(String path, String filter, int depth, boolean showHidden,
            boolean includeAcls, FILES_TYPE_FILTER types) {
        RepositoryRequest repoRequest = new RepositoryRequest();
        repoRequest.setPath(Utils.isEmpty(path) ? "/" : path);
        repoRequest.setChildNodeFilter(filter == null ? "*" : filter);
        repoRequest.setDepth(depth);
        repoRequest.setShowHidden(showHidden);
        repoRequest.setIncludeAcls(includeAcls);
        repoRequest.setTypes(types == null ? FILES_TYPE_FILTER.FILES_FOLDERS : types);

        RepositoryFileTree fileTree = pur.getTree(repoRequest);
        return fileTree;
    }

    // copies repo objects into folder struct on left
    private RepositoryDirectoryInterface copyFrom(RepositoryDirectoryInterface folders,
            RepositoryDirectoryInterface withFiles) {
        if (folders.getName().equals(withFiles.getName())) {
            for (RepositoryDirectoryInterface dir2 : withFiles.getChildren()) {
                for (RepositoryDirectoryInterface dir1 : folders.getChildren()) {
                    copyFrom(dir1, dir2);
                }
            }
            folders.setRepositoryObjects(withFiles.getRepositoryObjects());
        }
        return folders;
    }

    @Deprecated
    @Override
    public RepositoryDirectoryInterface loadRepositoryDirectoryTree(boolean eager) throws KettleException {

        // this method forces a reload of the repository directory tree structure
        // a new rootRef will be obtained - this is a SoftReference which will be used
        // by any calls to getRootDir()

        RepositoryDirectoryInterface rootDir;
        if (eager) {
            RepositoryFileTree rootFileTree = loadRepositoryFileTree(ClientRepositoryPaths.getRootFolderPath());
            rootDir = initRepositoryDirectoryTree(rootFileTree);
        } else {
            RepositoryFile root = pur.getFile("/");

            rootDir = new LazyUnifiedRepositoryDirectory(root, null, pur, purRepositoryServiceRegistry);
        }
        rootRef.setRef(rootDir);
        return rootDir;
    }

    @Override
    public RepositoryDirectoryInterface loadRepositoryDirectoryTree() throws KettleException {
        return loadRepositoryDirectoryTree(isLoadingEager());
    }

    private boolean isLoadingEager() {
        return "false".equals(System.getProperty(LAZY_REPOSITORY));
    }

    private RepositoryDirectoryInterface initRepositoryDirectoryTree(RepositoryFileTree repoTree)
            throws KettleException {
        RepositoryFile rootFolder = repoTree.getFile();
        RepositoryDirectory rootDir = new RepositoryDirectory();
        rootDir.setObjectId(new StringObjectId(rootFolder.getId().toString()));
        fillRepositoryDirectoryFromTree(rootDir, repoTree);

        // Example: /etc
        RepositoryDirectory etcDir = rootDir.findDirectory(ClientRepositoryPaths.getEtcFolderPath());

        RepositoryDirectory newRoot = new RepositoryDirectory();
        newRoot.setObjectId(rootDir.getObjectId());
        newRoot.setVisible(false);

        for (int i = 0; i < rootDir.getNrSubdirectories(); i++) {
            RepositoryDirectory childDir = rootDir.getSubdirectory(i);
            // Don't show /etc
            boolean isEtcChild = childDir.equals(etcDir);
            if (isEtcChild) {
                continue;
            }
            newRoot.addSubdirectory(childDir);
        }
        return newRoot;
    }

    private void fillRepositoryDirectoryFromTree(final RepositoryDirectoryInterface parentDir,
            final RepositoryFileTree treeNode) throws KettleException {
        try {
            List<RepositoryElementMetaInterface> fileChildren = new ArrayList<RepositoryElementMetaInterface>();
            List<RepositoryFileTree> children = treeNode.getChildren();
            if (children != null) {
                for (RepositoryFileTree child : children) {
                    if (child.getFile().isFolder()) {
                        RepositoryDirectory dir = new RepositoryDirectory(parentDir, child.getFile().getName());
                        dir.setObjectId(new StringObjectId(child.getFile().getId().toString()));
                        parentDir.addSubdirectory(dir);
                        fillRepositoryDirectoryFromTree(dir, child);
                    } else {
                        // a real file, like a Transformation or Job
                        RepositoryLock lock = unifiedRepositoryLockService.getLock(child.getFile());
                        RepositoryObjectType objectType = getObjectType(child.getFile().getName());

                        fileChildren
                                .add(new EERepositoryObject(child, parentDir, null, objectType, null, lock, false));
                    }
                }
                parentDir.setRepositoryObjects(fileChildren);
            }
        } catch (Exception e) {
            throw new KettleException("Unable to load directory structure from repository", e);
        }
    }

    @Override
    public String[] getDirectoryNames(final ObjectId idDirectory) throws KettleException {
        try {
            List<RepositoryFile> children = pur.getChildren(idDirectory.getId());
            List<String> childNames = new ArrayList<String>();
            for (RepositoryFile child : children) {
                if (child.isFolder()) {
                    childNames.add(child.getName());
                }
            }
            return childNames.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get list of object names from directory [" + idDirectory + "]", e);
        }
    }

    @Override
    public void deleteClusterSchema(ObjectId idCluster) throws KettleException {
        permanentlyDeleteSharedObject(idCluster);
        removeFromSharedObjectCache(RepositoryObjectType.CLUSTER_SCHEMA, idCluster);
    }

    @Override
    public void deleteJob(ObjectId idJob) throws KettleException {
        deleteFileById(idJob);
    }

    protected void permanentlyDeleteSharedObject(final ObjectId id) throws KettleException {
        try {
            pur.deleteFile(id.getId(), true, null);
        } catch (Exception e) {
            throw new KettleException("Unable to delete object with id [" + id + "]", e);
        }
    }

    public void deleteFileById(final ObjectId id) throws KettleException {
        try {
            pur.deleteFile(id.getId(), null);
            rootRef.clearRef();
        } catch (Exception e) {
            throw new KettleException("Unable to delete object with id [" + id + "]", e);
        }
    }

    @Override
    public void deletePartitionSchema(ObjectId idPartitionSchema) throws KettleException {
        permanentlyDeleteSharedObject(idPartitionSchema);
        removeFromSharedObjectCache(RepositoryObjectType.PARTITION_SCHEMA, idPartitionSchema);
    }

    @Override
    public void deleteSlave(ObjectId idSlave) throws KettleException {
        permanentlyDeleteSharedObject(idSlave);
        removeFromSharedObjectCache(RepositoryObjectType.SLAVE_SERVER, idSlave);
    }

    @Override
    public void deleteTransformation(ObjectId idTransformation) throws KettleException {
        deleteFileById(idTransformation);
        rootRef.clearRef();
    }

    @Override
    public boolean exists(final String name, final RepositoryDirectoryInterface repositoryDirectory,
            final RepositoryObjectType objectType) throws KettleException {
        try {
            String absPath = getPath(name, repositoryDirectory, objectType);
            return pur.getFile(absPath) != null;
        } catch (Exception e) {
            throw new KettleException("Unable to verify if the repository element [" + name + "] exists in ", e);
        }
    }

    private String getPath(final String name, final RepositoryDirectoryInterface repositoryDirectory,
            final RepositoryObjectType objectType) {

        String path = null;

        // need to check for null id since shared objects return a non-null repoDir (see
        // partSchema.getRepositoryDirectory())
        if (repositoryDirectory != null && repositoryDirectory.getObjectId() != null) {
            path = repositoryDirectory.getPath();
        }

        // return the directory path
        if (objectType == null) {
            return path;
        }

        String sanitizedName = checkAndSanitize(name);

        switch (objectType) {
        case DATABASE: {
            return getDatabaseMetaParentFolderPath() + RepositoryFile.SEPARATOR + sanitizedName
                    + RepositoryObjectType.DATABASE.getExtension();
        }
        case TRANSFORMATION: {
            // Check for null path
            if (path == null) {
                return null;
            } else {
                return path + (path.endsWith(RepositoryFile.SEPARATOR) ? "" : RepositoryFile.SEPARATOR)
                        + sanitizedName + RepositoryObjectType.TRANSFORMATION.getExtension();
            }
        }
        case PARTITION_SCHEMA: {
            return getPartitionSchemaParentFolderPath() + RepositoryFile.SEPARATOR + sanitizedName
                    + RepositoryObjectType.PARTITION_SCHEMA.getExtension();
        }
        case SLAVE_SERVER: {
            return getSlaveServerParentFolderPath() + RepositoryFile.SEPARATOR + sanitizedName
                    + RepositoryObjectType.SLAVE_SERVER.getExtension();
        }
        case CLUSTER_SCHEMA: {
            return getClusterSchemaParentFolderPath() + RepositoryFile.SEPARATOR + sanitizedName
                    + RepositoryObjectType.CLUSTER_SCHEMA.getExtension();
        }
        case JOB: {
            // Check for null path
            if (path == null) {
                return null;
            } else {
                return path + (path.endsWith(RepositoryFile.SEPARATOR) ? "" : RepositoryFile.SEPARATOR)
                        + sanitizedName + RepositoryObjectType.JOB.getExtension();
            }
        }
        default: {
            throw new UnsupportedOperationException("not implemented");
        }
        }
    }

    @Override
    public ObjectId getClusterID(String name) throws KettleException {
        try {
            return getObjectId(name, null, RepositoryObjectType.CLUSTER_SCHEMA, false);
        } catch (Exception e) {
            throw new KettleException("Unable to get ID for cluster schema [" + name + "]", e);
        }
    }

    @Override
    public ObjectId[] getClusterIDs(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.CLUSTER_SCHEMA,
                    includeDeleted);
            List<ObjectId> ids = new ArrayList<ObjectId>();
            for (RepositoryFile file : children) {
                ids.add(new StringObjectId(file.getId().toString()));
            }
            return ids.toArray(new ObjectId[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all cluster schema IDs", e);
        }
    }

    @Override
    public String[] getClusterNames(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.CLUSTER_SCHEMA,
                    includeDeleted);
            List<String> names = new ArrayList<String>();
            for (RepositoryFile file : children) {
                names.add(file.getTitle());
            }
            return names.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all cluster schema names", e);
        }
    }

    @Override
    public ObjectId getDatabaseID(final String name) throws KettleException {
        try {
            ObjectId objectId = getObjectId(name, null, RepositoryObjectType.DATABASE, false);
            if (objectId == null) {
                List<RepositoryFile> allDatabases = getAllFilesOfType(null, RepositoryObjectType.DATABASE, false);
                String[] existingNames = new String[allDatabases.size()];
                for (int i = 0; i < allDatabases.size(); i++) {
                    RepositoryFile file = allDatabases.get(i);
                    existingNames[i] = file.getTitle();
                }
                int index = DatabaseMeta.indexOfName(existingNames, name);
                if (index != -1) {
                    return new StringObjectId(allDatabases.get(index).getId().toString());
                }
            }
            return objectId;
        } catch (Exception e) {
            throw new KettleException("Unable to get ID for database [" + name + "]", e);
        }
    }

    /**
     * Copying the behavior of the original JCRRepository, this implementation returns IDs of deleted objects too.
     */
    private ObjectId getObjectId(final String name, final RepositoryDirectoryInterface dir,
            final RepositoryObjectType objectType, boolean includedDeleteFiles) {
        final String absPath = getPath(name, dir, objectType);
        RepositoryFile file = pur.getFile(absPath);
        if (file != null) {
            // file exists
            return new StringObjectId(file.getId().toString());
        } else if (includedDeleteFiles) {
            switch (objectType) {
            case DATABASE: {
                // file either never existed or has been deleted
                List<RepositoryFile> deletedChildren = pur.getDeletedFiles(getDatabaseMetaParentFolderPath(),
                        name + RepositoryObjectType.DATABASE.getExtension());
                if (!deletedChildren.isEmpty()) {
                    return new StringObjectId(deletedChildren.get(0).getId().toString());
                } else {
                    return null;
                }
            }
            case TRANSFORMATION: {
                // file either never existed or has been deleted
                List<RepositoryFile> deletedChildren = pur.getDeletedFiles(dir.getObjectId().getId(),
                        name + RepositoryObjectType.TRANSFORMATION.getExtension());
                if (!deletedChildren.isEmpty()) {
                    return new StringObjectId(deletedChildren.get(0).getId().toString());
                } else {
                    return null;
                }
            }
            case PARTITION_SCHEMA: {
                // file either never existed or has been deleted
                List<RepositoryFile> deletedChildren = pur.getDeletedFiles(getPartitionSchemaParentFolderPath(),
                        name + RepositoryObjectType.PARTITION_SCHEMA.getExtension());
                if (!deletedChildren.isEmpty()) {
                    return new StringObjectId(deletedChildren.get(0).getId().toString());
                } else {
                    return null;
                }
            }
            case SLAVE_SERVER: {
                // file either never existed or has been deleted
                List<RepositoryFile> deletedChildren = pur.getDeletedFiles(getSlaveServerParentFolderPath(),
                        name + RepositoryObjectType.SLAVE_SERVER.getExtension());
                if (!deletedChildren.isEmpty()) {
                    return new StringObjectId(deletedChildren.get(0).getId().toString());
                } else {
                    return null;
                }
            }
            case CLUSTER_SCHEMA: {
                // file either never existed or has been deleted
                List<RepositoryFile> deletedChildren = pur.getDeletedFiles(getClusterSchemaParentFolderPath(),
                        name + RepositoryObjectType.CLUSTER_SCHEMA.getExtension());
                if (!deletedChildren.isEmpty()) {
                    return new StringObjectId(deletedChildren.get(0).getId().toString());
                } else {
                    return null;
                }
            }
            case JOB: {
                // file either never existed or has been deleted
                List<RepositoryFile> deletedChildren = pur.getDeletedFiles(dir.getObjectId().getId(),
                        name + RepositoryObjectType.JOB.getExtension());
                if (!deletedChildren.isEmpty()) {
                    return new StringObjectId(deletedChildren.get(0).getId().toString());
                } else {
                    return null;
                }
            }
            default: {
                throw new UnsupportedOperationException("not implemented");
            }
            }
        } else {
            return null;
        }
    }

    @Override
    public ObjectId[] getDatabaseIDs(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.DATABASE, includeDeleted);
            List<ObjectId> ids = new ArrayList<ObjectId>(children.size());
            for (RepositoryFile file : children) {
                ids.add(new StringObjectId(file.getId().toString()));
            }
            return ids.toArray(new ObjectId[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all database IDs", e);
        }
    }

    protected List<RepositoryFile> getAllFilesOfType(final ObjectId dirId, final RepositoryObjectType objectType,
            final boolean includeDeleted) throws KettleException {
        return getAllFilesOfType(dirId, Collections.singletonList(objectType), includeDeleted);
    }

    protected List<RepositoryFile> getAllFilesOfType(final ObjectId dirId,
            final List<RepositoryObjectType> objectTypes, final boolean includeDeleted) throws KettleException {

        List<RepositoryFile> allChildren = new ArrayList<RepositoryFile>();
        List<RepositoryFile> children = getAllFilesOfType(dirId, objectTypes);
        allChildren.addAll(children);
        if (includeDeleted) {
            String dirPath = null;
            if (dirId != null) {
                // derive path using id
                dirPath = pur.getFileById(dirId.getId()).getPath();
            }
            List<RepositoryFile> deletedChildren = getAllDeletedFilesOfType(dirPath, objectTypes);
            allChildren.addAll(deletedChildren);
            Collections.sort(allChildren);
        }
        return allChildren;
    }

    protected List<RepositoryFile> getAllFilesOfType(final ObjectId dirId,
            final List<RepositoryObjectType> objectTypes) throws KettleException {
        Set<Serializable> parentFolderIds = new HashSet<>();
        List<String> filters = new ArrayList<>();
        for (RepositoryObjectType objectType : objectTypes) {
            switch (objectType) {
            case DATABASE: {
                parentFolderIds.add(getDatabaseMetaParentFolderId());
                filters.add("*" + RepositoryObjectType.DATABASE.getExtension()); //$NON-NLS-1$
                break;
            }
            case TRANSFORMATION: {
                parentFolderIds.add(dirId.getId());
                filters.add("*" + RepositoryObjectType.TRANSFORMATION.getExtension()); //$NON-NLS-1$
                break;
            }
            case PARTITION_SCHEMA: {
                parentFolderIds.add(getPartitionSchemaParentFolderId());
                filters.add("*" + RepositoryObjectType.PARTITION_SCHEMA.getExtension()); //$NON-NLS-1$
                break;
            }
            case SLAVE_SERVER: {
                parentFolderIds.add(getSlaveServerParentFolderId());
                filters.add("*" + RepositoryObjectType.SLAVE_SERVER.getExtension()); //$NON-NLS-1$
                break;
            }
            case CLUSTER_SCHEMA: {
                parentFolderIds.add(getClusterSchemaParentFolderId());
                filters.add("*" + RepositoryObjectType.CLUSTER_SCHEMA.getExtension()); //$NON-NLS-1$
                break;
            }
            case JOB: {
                parentFolderIds.add(dirId.getId());
                filters.add("*" + RepositoryObjectType.JOB.getExtension()); //$NON-NLS-1$
                break;
            }
            case TRANS_DATA_SERVICE: {
                parentFolderIds.add(dirId.getId());
                filters.add("*" + RepositoryObjectType.TRANS_DATA_SERVICE.getExtension()); //$NON-NLS-1$
                break;
            }
            default: {
                throw new UnsupportedOperationException("not implemented");
            }
            }
        }
        StringBuilder mergedFilterBuf = new StringBuilder();
        // build filter
        int i = 0;
        for (String filter : filters) {
            if (i++ > 0) {
                mergedFilterBuf.append(" | "); //$NON-NLS-1$
            }
            mergedFilterBuf.append(filter);
        }
        List<RepositoryFile> allFiles = new ArrayList<>();
        for (Serializable parentFolderId : parentFolderIds) {
            allFiles.addAll(pur.getChildren(parentFolderId, mergedFilterBuf.toString()));
        }
        Collections.sort(allFiles);
        return allFiles;
    }

    protected List<RepositoryFile> getAllDeletedFilesOfType(final String dirPath,
            final List<RepositoryObjectType> objectTypes) throws KettleException {
        Set<String> parentFolderPaths = new HashSet<>();
        List<String> filters = new ArrayList<>();
        for (RepositoryObjectType objectType : objectTypes) {
            switch (objectType) {
            case DATABASE: {
                parentFolderPaths.add(getDatabaseMetaParentFolderPath());
                filters.add("*" + RepositoryObjectType.DATABASE.getExtension()); //$NON-NLS-1$
                break;
            }
            case TRANSFORMATION: {
                parentFolderPaths.add(dirPath);
                filters.add("*" + RepositoryObjectType.TRANSFORMATION.getExtension()); //$NON-NLS-1$
                break;
            }
            case PARTITION_SCHEMA: {
                parentFolderPaths.add(getPartitionSchemaParentFolderPath());
                filters.add("*" + RepositoryObjectType.PARTITION_SCHEMA.getExtension()); //$NON-NLS-1$
                break;
            }
            case SLAVE_SERVER: {
                parentFolderPaths.add(getSlaveServerParentFolderPath());
                filters.add("*" + RepositoryObjectType.SLAVE_SERVER.getExtension()); //$NON-NLS-1$
                break;
            }
            case CLUSTER_SCHEMA: {
                parentFolderPaths.add(getClusterSchemaParentFolderPath());
                filters.add("*" + RepositoryObjectType.CLUSTER_SCHEMA.getExtension()); //$NON-NLS-1$
                break;
            }
            case JOB: {
                parentFolderPaths.add(dirPath);
                filters.add("*" + RepositoryObjectType.JOB.getExtension()); //$NON-NLS-1$
                break;
            }
            default: {
                throw new UnsupportedOperationException();
            }
            }
        }
        StringBuilder mergedFilterBuf = new StringBuilder();
        // build filter
        int i = 0;
        for (String filter : filters) {
            if (i++ > 0) {
                mergedFilterBuf.append(" | "); //$NON-NLS-1$
            }
            mergedFilterBuf.append(filter);
        }
        List<RepositoryFile> allFiles = new ArrayList<RepositoryFile>();
        for (String parentFolderPath : parentFolderPaths) {
            allFiles.addAll(pur.getDeletedFiles(parentFolderPath, mergedFilterBuf.toString()));
        }
        Collections.sort(allFiles);
        return allFiles;
    }

    @Override
    public String[] getDatabaseNames(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.DATABASE, includeDeleted);
            List<String> names = new ArrayList<String>(children.size());
            for (RepositoryFile file : children) {
                names.add(file.getTitle());
            }
            return names.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all database names", e);
        }
    }

    /**
     * Initialize the shared object assembler map with known assemblers
     */
    private void initSharedObjectAssemblerMap() {
        sharedObjectAssemblerMap = new EnumMap<RepositoryObjectType, SharedObjectAssembler<?>>(
                RepositoryObjectType.class);
        sharedObjectAssemblerMap.put(RepositoryObjectType.DATABASE, databaseMetaTransformer);
        sharedObjectAssemblerMap.put(RepositoryObjectType.CLUSTER_SCHEMA, clusterTransformer);
        sharedObjectAssemblerMap.put(RepositoryObjectType.PARTITION_SCHEMA, partitionSchemaTransformer);
        sharedObjectAssemblerMap.put(RepositoryObjectType.SLAVE_SERVER, slaveTransformer);
    }

    public DatabaseDelegate getDatabaseMetaTransformer() {
        return databaseMetaTransformer;
    }

    public ClusterDelegate getClusterTransformer() {
        return clusterTransformer;
    }

    public PartitionDelegate getPartitionSchemaTransformer() {
        return partitionSchemaTransformer;
    }

    @Override
    public void clearSharedObjectCache() {
        sharedObjectsByType = null;
    }

    /**
     * Read shared objects of the types provided from the repository. Every {@link SharedObjectInterface} that is read
     * will be fully loaded as if it has been loaded through {@link #loadDatabaseMeta(ObjectId, String)},
     * {@link #loadClusterSchema(ObjectId, List, String)}, etc.
     * <p>
     * This method was introduced to reduce the number of server calls for loading shared objects to a constant number:
     * {@code 2 + n, where n is the number of types requested}.
     * </p>
     *
     * @param sharedObjectsByType
     *          Map of type to shared objects. Each map entry will contain a non-null {@link List} of
     *          {@link RepositoryObjectType}s for every type provided. Only entries for types provided will be altered.
     * @param types
     *          Types of repository objects to read from the repository
     * @throws KettleException
     */
    protected void readSharedObjects(
            Map<RepositoryObjectType, List<? extends SharedObjectInterface>> sharedObjectsByType,
            RepositoryObjectType... types) throws KettleException {
        // Overview:
        // 1) We will fetch RepositoryFile, NodeRepositoryFileData, and VersionSummary for all types provided.
        // 2) We assume that unless an exception is thrown every RepositoryFile returned by getFilesByType(..) have a
        // matching NodeRepositoryFileData and VersionSummary.
        // 3) With all files, node data, and versions in hand we will iterate over them, merging them back into usable
        // shared objects
        List<RepositoryFile> allFiles = new ArrayList<RepositoryFile>();
        // Since type is not preserved in the RepositoryFile we fetch files by type so we don't rely on parsing the name to
        // determine type afterward
        // Map must be ordered or we can't match up files with data and version summary
        LinkedHashMap<RepositoryObjectType, List<RepositoryFile>> filesByType = getFilesByType(allFiles, types);
        try {
            List<NodeRepositoryFileData> data = pur.getDataForReadInBatch(allFiles, NodeRepositoryFileData.class);
            List<VersionSummary> versions = pur.getVersionSummaryInBatch(allFiles);
            // Only need one iterator for all data and versions. We will work through them as we process the files by type, in
            // order.
            Iterator<NodeRepositoryFileData> dataIter = data.iterator();
            Iterator<VersionSummary> versionsIter = versions.iterator();

            // Assemble into completely loaded SharedObjectInterfaces by type
            for (Entry<RepositoryObjectType, List<RepositoryFile>> entry : filesByType.entrySet()) {
                SharedObjectAssembler<?> assembler = sharedObjectAssemblerMap.get(entry.getKey());
                if (assembler == null) {
                    throw new UnsupportedOperationException(
                            String.format("Cannot assemble shared object of type [%s]", entry.getKey())); //$NON-NLS-1$
                }
                // For all files of this type, assemble them from the pieces of data pulled from the repository
                Iterator<RepositoryFile> filesIter = entry.getValue().iterator();
                List<SharedObjectInterface> sharedObjects = new ArrayList<SharedObjectInterface>(
                        entry.getValue().size());
                // Exceptions are thrown during lookup if data or versions aren't found so all the lists should be the same size
                // (no need to check for next on all iterators)
                while (filesIter.hasNext()) {
                    RepositoryFile file = filesIter.next();
                    NodeRepositoryFileData repoData = dataIter.next();
                    VersionSummary version = versionsIter.next();

                    // TODO: inexistent db types can cause exceptions assembling; prevent total failure
                    try {
                        sharedObjects.add(assembler.assemble(file, repoData, version));
                    } catch (Exception ex) {
                        // TODO i18n
                        getLog().logError("Unable to load shared objects", ex);
                    }
                }
                sharedObjectsByType.put(entry.getKey(), sharedObjects);
            }
        } catch (Exception ex) {
            // TODO i18n
            throw new KettleException("Unable to load shared objects", ex); //$NON-NLS-1$
        }
    }

    /**
     * Fetch {@link RepositoryFile}s by {@code RepositoryObjectType}.
     *
     * @param allFiles
     *          List to add files into.
     * @param types
     *          Types of files to fetch
     * @return Ordered map of object types to list of files.
     * @throws KettleException
     */
    private LinkedHashMap<RepositoryObjectType, List<RepositoryFile>> getFilesByType(List<RepositoryFile> allFiles,
            RepositoryObjectType... types) throws KettleException {
        // Must be ordered or we can't match up files with data and version summary
        LinkedHashMap<RepositoryObjectType, List<RepositoryFile>> filesByType = new LinkedHashMap<RepositoryObjectType, List<RepositoryFile>>();
        // Since type is not preserved in the RepositoryFile we must fetch files by type
        for (RepositoryObjectType type : types) {
            try {
                List<RepositoryFile> files = getAllFilesOfType(null, type, false);
                filesByType.put(type, files);
                allFiles.addAll(files);
            } catch (Exception ex) {
                // TODO i18n
                throw new KettleException(String.format("Unable to get all files of type [%s]", type), ex); //$NON-NLS-1$
            }
        }
        return filesByType;
    }

    @Override
    public List<DatabaseMeta> readDatabases() throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.DATABASE, false);
            List<DatabaseMeta> dbMetas = new ArrayList<DatabaseMeta>();
            for (RepositoryFile file : children) {
                DataNode node = pur.getDataForRead(file.getId(), NodeRepositoryFileData.class).getNode();
                DatabaseMeta databaseMeta = (DatabaseMeta) databaseMetaTransformer.dataNodeToElement(node);
                databaseMeta.setName(file.getTitle());
                dbMetas.add(databaseMeta);
            }
            return dbMetas;
        } catch (Exception e) {
            throw new KettleException("Unable to read all databases", e);
        }
    }

    @Override
    public void deleteDatabaseMeta(final String databaseName) throws KettleException {
        RepositoryFile fileToDelete = null;
        try {
            fileToDelete = pur.getFile(getPath(databaseName, null, RepositoryObjectType.DATABASE));
        } catch (Exception e) {
            throw new KettleException("Unable to delete database with name [" + databaseName + "]", e);
        }
        ObjectId idDatabase = new StringObjectId(fileToDelete.getId().toString());
        permanentlyDeleteSharedObject(idDatabase);
        removeFromSharedObjectCache(RepositoryObjectType.DATABASE, idDatabase);
    }

    @Override
    public long getJobEntryAttributeInteger(ObjectId idJobentry, int nr, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public String getJobEntryAttributeString(ObjectId idJobentry, int nr, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean getJobEntryAttributeBoolean(ObjectId arg0, int arg1, String arg2, boolean arg3)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public ObjectId getJobId(final String name, final RepositoryDirectoryInterface repositoryDirectory)
            throws KettleException {
        try {
            return getObjectId(name, repositoryDirectory, RepositoryObjectType.JOB, false);
        } catch (Exception e) {
            String path = repositoryDirectory != null ? repositoryDirectory.toString() : "null";
            throw new IdNotFoundException("Unable to get ID for job [" + name + "]", e, name, path,
                    RepositoryObjectType.JOB);
        }
    }

    @Override
    public String[] getJobNames(ObjectId idDirectory, boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(idDirectory, RepositoryObjectType.JOB,
                    includeDeleted);
            List<String> names = new ArrayList<String>();
            for (RepositoryFile file : children) {
                names.add(file.getTitle());
            }
            return names.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all job names", e);
        }
    }

    @Override
    public List<RepositoryElementMetaInterface> getJobObjects(ObjectId idDirectory, boolean includeDeleted)
            throws KettleException {
        return getPdiObjects(idDirectory, Arrays.asList(new RepositoryObjectType[] { RepositoryObjectType.JOB }),
                includeDeleted);
    }

    @Override
    public LogChannelInterface getLog() {
        return log;
    }

    @Override
    public String getName() {
        return repositoryMeta.getName();
    }

    @Override
    public ObjectId getPartitionSchemaID(String name) throws KettleException {
        try {
            return getObjectId(name, null, RepositoryObjectType.PARTITION_SCHEMA, false);
        } catch (Exception e) {
            throw new KettleException("Unable to get ID for partition schema [" + name + "]", e);
        }
    }

    @Override
    public ObjectId[] getPartitionSchemaIDs(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.PARTITION_SCHEMA,
                    includeDeleted);
            List<ObjectId> ids = new ArrayList<ObjectId>();
            for (RepositoryFile file : children) {
                ids.add(new StringObjectId(file.getId().toString()));
            }
            return ids.toArray(new ObjectId[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all partition schema IDs", e);
        }
    }

    @Override
    public String[] getPartitionSchemaNames(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.PARTITION_SCHEMA,
                    includeDeleted);
            List<String> names = new ArrayList<String>();
            for (RepositoryFile file : children) {
                names.add(file.getTitle());
            }
            return names.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all partition schema names", e);
        }
    }

    @Override
    public RepositoryMeta getRepositoryMeta() {
        return repositoryMeta;
    }

    @Override
    public RepositorySecurityProvider getSecurityProvider() {
        return securityProvider;
    }

    @Override
    public RepositorySecurityManager getSecurityManager() {
        return securityManager;
    }

    @Override
    public ObjectId getSlaveID(String name) throws KettleException {
        try {
            return getObjectId(name, null, RepositoryObjectType.SLAVE_SERVER, false);
        } catch (Exception e) {
            throw new KettleException("Unable to get ID for slave server with name [" + name + "]", e);
        }
    }

    @Override
    public ObjectId[] getSlaveIDs(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.SLAVE_SERVER,
                    includeDeleted);
            List<ObjectId> ids = new ArrayList<ObjectId>();
            for (RepositoryFile file : children) {
                ids.add(new StringObjectId(file.getId().toString()));
            }
            return ids.toArray(new ObjectId[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all slave server IDs", e);
        }
    }

    @Override
    public String[] getSlaveNames(boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(null, RepositoryObjectType.SLAVE_SERVER,
                    includeDeleted);
            List<String> names = new ArrayList<String>();
            for (RepositoryFile file : children) {
                names.add(file.getTitle());
            }
            return names.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all slave server names", e);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<SlaveServer> getSlaveServers() throws KettleException {
        return (List<SlaveServer>) loadAndCacheSharedObjects(true).get(RepositoryObjectType.SLAVE_SERVER);
    }

    public SlaveDelegate getSlaveTransformer() {
        return slaveTransformer;
    }

    @Override
    public boolean getStepAttributeBoolean(ObjectId idStep, int nr, String code, boolean def)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public long getStepAttributeInteger(ObjectId idStep, int nr, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public String getStepAttributeString(ObjectId idStep, int nr, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public ObjectId getTransformationID(String name, RepositoryDirectoryInterface repositoryDirectory)
            throws KettleException {
        try {
            return getObjectId(name, repositoryDirectory, RepositoryObjectType.TRANSFORMATION, false);
        } catch (Exception e) {
            String path = repositoryDirectory != null ? repositoryDirectory.toString() : "null";
            throw new IdNotFoundException("Unable to get ID for job [" + name + "]", e, name, path,
                    RepositoryObjectType.TRANSFORMATION);
        }
    }

    @Override
    public String[] getTransformationNames(ObjectId idDirectory, boolean includeDeleted) throws KettleException {
        try {
            List<RepositoryFile> children = getAllFilesOfType(idDirectory, RepositoryObjectType.TRANSFORMATION,
                    includeDeleted);
            List<String> names = new ArrayList<String>();
            for (RepositoryFile file : children) {
                names.add(file.getTitle());
            }
            return names.toArray(new String[0]);
        } catch (Exception e) {
            throw new KettleException("Unable to get all transformation names", e);
        }
    }

    @Override
    public List<RepositoryElementMetaInterface> getTransformationObjects(ObjectId idDirectory,
            boolean includeDeleted) throws KettleException {
        return getPdiObjects(idDirectory,
                Arrays.asList(new RepositoryObjectType[] { RepositoryObjectType.TRANSFORMATION }), includeDeleted);
    }

    protected List<RepositoryElementMetaInterface> getPdiObjects(ObjectId dirId,
            List<RepositoryObjectType> objectTypes, boolean includeDeleted) throws KettleException {
        try {

            RepositoryDirectoryInterface repDir = getRootDir().findDirectory(dirId);

            List<RepositoryElementMetaInterface> list = new ArrayList<RepositoryElementMetaInterface>();
            List<RepositoryFile> nonDeletedChildren = getAllFilesOfType(dirId, objectTypes);
            for (RepositoryFile file : nonDeletedChildren) {
                RepositoryLock lock = unifiedRepositoryLockService.getLock(file);
                RepositoryObjectType objectType = getObjectType(file.getName());

                list.add(new EERepositoryObject(file, repDir, null, objectType, null, lock, false));
            }
            if (includeDeleted) {
                String dirPath = null;
                if (dirId != null) {
                    // derive path using id
                    dirPath = pur.getFileById(dirId.getId()).getPath();
                }
                List<RepositoryFile> deletedChildren = getAllDeletedFilesOfType(dirPath, objectTypes);
                for (RepositoryFile file : deletedChildren) {
                    RepositoryLock lock = unifiedRepositoryLockService.getLock(file);
                    RepositoryObjectType objectType = getObjectType(file.getName());
                    list.add(new EERepositoryObject(file, repDir, null, objectType, null, lock, true));
                }
            }
            return list;
        } catch (Exception e) {
            throw new KettleException("Unable to get list of objects from directory [" + dirId + "]", e);
        }
    }

    public static RepositoryObjectType getObjectType(final String filename) throws KettleException {
        if (filename.endsWith(RepositoryObjectType.TRANSFORMATION.getExtension())) {
            return RepositoryObjectType.TRANSFORMATION;
        } else if (filename.endsWith(RepositoryObjectType.JOB.getExtension())) {
            return RepositoryObjectType.JOB;
        } else if (filename.endsWith(RepositoryObjectType.DATABASE.getExtension())) {
            return RepositoryObjectType.DATABASE;
        } else if (filename.endsWith(RepositoryObjectType.SLAVE_SERVER.getExtension())) {
            return RepositoryObjectType.SLAVE_SERVER;
        } else if (filename.endsWith(RepositoryObjectType.CLUSTER_SCHEMA.getExtension())) {
            return RepositoryObjectType.CLUSTER_SCHEMA;
        } else if (filename.endsWith(RepositoryObjectType.PARTITION_SCHEMA.getExtension())) {
            return RepositoryObjectType.PARTITION_SCHEMA;
        } else {
            return RepositoryObjectType.UNKNOWN;
        }
    }

    @Override
    public IUser getUserInfo() {
        return user;
    }

    @Override
    public String getVersion() {
        return REPOSITORY_VERSION;
    }

    @Override
    public void insertJobEntryDatabase(ObjectId idJob, ObjectId idJobentry, ObjectId idDatabase)
            throws KettleException {
        throw new UnsupportedOperationException();
    }

    @Override
    public ObjectId insertLogEntry(String description) throws KettleException {
        // We are not presently logging
        return null;
    }

    @Override
    public void insertStepDatabase(ObjectId idTransformation, ObjectId idStep, ObjectId idDatabase)
            throws KettleException {
        throw new UnsupportedOperationException();
    }

    @Override
    public ClusterSchema loadClusterSchema(ObjectId idClusterSchema, List<SlaveServer> slaveServers,
            String versionId) throws KettleException {
        try {
            // We dont need to use slaveServer variable as the dataNoteToElement method finds the server from the repository
            NodeRepositoryFileData data = pur.getDataAtVersionForRead(idClusterSchema.getId(), versionId,
                    NodeRepositoryFileData.class);
            RepositoryFile file = null;
            if (versionId != null) {
                file = pur.getFileAtVersion(idClusterSchema.getId(), versionId);
            } else {
                file = pur.getFileById(idClusterSchema.getId());
            }
            return clusterTransformer.assemble(file, data,
                    pur.getVersionSummary(idClusterSchema.getId(), versionId));
        } catch (Exception e) {
            throw new KettleException("Unable to load cluster schema with id [" + idClusterSchema + "]", e);
        }
    }

    @Override
    public Condition loadConditionFromStepAttribute(ObjectId idStep, String code) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public DatabaseMeta loadDatabaseMetaFromJobEntryAttribute(ObjectId idJobentry, String nameCode, int nr,
            String idCode, List<DatabaseMeta> databases) throws KettleException {
        throw new UnsupportedOperationException();
    }

    @Override
    public DatabaseMeta loadDatabaseMetaFromStepAttribute(ObjectId idStep, String code,
            List<DatabaseMeta> databases) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public PartitionSchema loadPartitionSchema(ObjectId partitionSchemaId, String versionId)
            throws KettleException {
        try {
            NodeRepositoryFileData data = pur.getDataAtVersionForRead(partitionSchemaId.getId(), versionId,
                    NodeRepositoryFileData.class);
            RepositoryFile file = null;
            if (versionId != null) {
                file = pur.getFileAtVersion(partitionSchemaId.getId(), versionId);
            } else {
                file = pur.getFileById(partitionSchemaId.getId());
            }
            return partitionSchemaTransformer.assemble(file, data,
                    pur.getVersionSummary(partitionSchemaId.getId(), versionId));
        } catch (Exception e) {
            throw new KettleException("Unable to load partition schema with id [" + partitionSchemaId + "]", e);
        }
    }

    @Override
    public SlaveServer loadSlaveServer(ObjectId idSlaveServer, String versionId) throws KettleException {
        try {
            NodeRepositoryFileData data = pur.getDataAtVersionForRead(idSlaveServer.getId(), versionId,
                    NodeRepositoryFileData.class);
            RepositoryFile file = null;
            if (versionId != null) {
                file = pur.getFileAtVersion(idSlaveServer.getId(), versionId);
            } else {
                file = pur.getFileById(idSlaveServer.getId());
            }
            return slaveTransformer.assemble(file, data, pur.getVersionSummary(idSlaveServer.getId(), versionId));
        } catch (Exception e) {
            throw new KettleException("Unable to load slave server with id [" + idSlaveServer + "]", e);
        }
    }

    protected Map<RepositoryObjectType, List<? extends SharedObjectInterface>> loadAndCacheSharedObjects(
            final boolean deepCopy) throws KettleException {
        if (sharedObjectsByType == null) {
            try {
                sharedObjectsByType = new EnumMap<RepositoryObjectType, List<? extends SharedObjectInterface>>(
                        RepositoryObjectType.class);
                // Slave Servers are referenced by Cluster Schemas so they must be loaded first
                readSharedObjects(sharedObjectsByType, RepositoryObjectType.DATABASE,
                        RepositoryObjectType.PARTITION_SCHEMA, RepositoryObjectType.SLAVE_SERVER,
                        RepositoryObjectType.CLUSTER_SCHEMA);
            } catch (Exception e) {
                sharedObjectsByType = null;
                // TODO i18n
                throw new KettleException("Unable to read shared objects from repository", e); //$NON-NLS-1$
            }
        }
        return deepCopy ? deepCopy(sharedObjectsByType) : sharedObjectsByType;
    }

    protected Map<RepositoryObjectType, List<? extends SharedObjectInterface>> loadAndCacheSharedObjects()
            throws KettleException {
        return loadAndCacheSharedObjects(true);
    }

    private Map<RepositoryObjectType, List<? extends SharedObjectInterface>> deepCopy(
            Map<RepositoryObjectType, List<? extends SharedObjectInterface>> orig) throws KettleException {
        Map<RepositoryObjectType, List<? extends SharedObjectInterface>> copy = new EnumMap<RepositoryObjectType, List<? extends SharedObjectInterface>>(
                RepositoryObjectType.class);
        for (Entry<RepositoryObjectType, List<? extends SharedObjectInterface>> entry : orig.entrySet()) {
            RepositoryObjectType type = entry.getKey();
            List<? extends SharedObjectInterface> value = entry.getValue();

            List<SharedObjectInterface> newValue = new ArrayList<SharedObjectInterface>(value.size());
            for (SharedObjectInterface obj : value) {
                SharedObjectInterface newValueItem;
                if (obj instanceof DatabaseMeta) {
                    DatabaseMeta databaseMeta = (DatabaseMeta) ((DatabaseMeta) obj).clone();
                    databaseMeta.setObjectId(((DatabaseMeta) obj).getObjectId());
                    databaseMeta.clearChanged();
                    newValueItem = databaseMeta;
                } else if (obj instanceof SlaveServer) {
                    SlaveServer slaveServer = (SlaveServer) ((SlaveServer) obj).clone();
                    slaveServer.setObjectId(((SlaveServer) obj).getObjectId());
                    slaveServer.clearChanged();
                    newValueItem = slaveServer;
                } else if (obj instanceof PartitionSchema) {
                    PartitionSchema partitionSchema = (PartitionSchema) ((PartitionSchema) obj).clone();
                    partitionSchema.setObjectId(((PartitionSchema) obj).getObjectId());
                    partitionSchema.clearChanged();
                    newValueItem = partitionSchema;
                } else if (obj instanceof ClusterSchema) {
                    ClusterSchema clusterSchema = ((ClusterSchema) obj).clone();
                    clusterSchema.setObjectId(((ClusterSchema) obj).getObjectId());
                    clusterSchema.clearChanged();
                    newValueItem = clusterSchema;
                } else {
                    throw new KettleException("unknown shared object class");
                }
                newValue.add(newValueItem);
            }
            copy.put(type, newValue);
        }
        return copy;
    }

    @Override
    public SharedObjects readJobMetaSharedObjects(final JobMeta jobMeta) throws KettleException {
        return jobDelegate.loadSharedObjects(jobMeta, loadAndCacheSharedObjects(true));
    }

    @Override
    public SharedObjects readTransSharedObjects(final TransMeta transMeta) throws KettleException {
        return transDelegate.loadSharedObjects(transMeta, loadAndCacheSharedObjects(true));
    }

    @Override
    public ObjectId renameJob(ObjectId idJob, RepositoryDirectoryInterface newDirectory, String newName)
            throws KettleException {
        return renameJob(idJob, null, newDirectory, newName);
    }

    @Override
    public ObjectId renameJob(ObjectId idJobForRename, String versionComment,
            RepositoryDirectoryInterface newDirectory, String newJobName) throws KettleException {
        return renameTransOrJob(idJobForRename, versionComment, newDirectory, newJobName, RepositoryObjectType.JOB,
                "PurRepository.ERROR_0006_UNABLE_TO_RENAME_JOB");
    }

    @Override
    public ObjectId renameTransformation(ObjectId idTransformation, RepositoryDirectoryInterface newDirectory,
            String newName) throws KettleException {
        return renameTransformation(idTransformation, null, newDirectory, newName);
    }

    @Override
    public ObjectId renameTransformation(ObjectId idTransForRename, String versionComment,
            RepositoryDirectoryInterface newDirectory, String newTransName) throws KettleException {
        return renameTransOrJob(idTransForRename, versionComment, newDirectory, newTransName,
                RepositoryObjectType.TRANSFORMATION, "PurRepository.ERROR_0006_UNABLE_TO_RENAME_TRANS");
    }

    /**
     * Renames and optionally moves a file having {@code idObject}. If {@code newDirectory} is <tt>null</tt>, then the
     * file is just renamed. If {@code newTitle} is <tt>null</tt>, then the file should keep its name.
     * <p/>
     * Note, it is expected that the file exists
     *
     * @param idObject
     *          file's id
     * @param versionComment
     *          comment on the revision
     * @param newDirectory
     *          new folder, where to move the file; <tt>null</tt> means the file should be left in its current
     * @param newTitle
     *          new file's title (title is a name w/o extension); <tt>null</tt> means the file should keep its current
     * @param objectType
     *          file's type; {@linkplain RepositoryObjectType#TRANSFORMATION} or {@linkplain RepositoryObjectType#JOB} are
     *          expected
     * @param errorMsgKey
     *          key for the error message passed with the exception
     * @throws KettleException
     *           if file with same path exists
     */
    private ObjectId renameTransOrJob(ObjectId idObject, String versionComment,
            RepositoryDirectoryInterface newDirectory, String newTitle, RepositoryObjectType objectType,
            String errorMsgKey) throws KettleException {

        RepositoryFile file = pur.getFileById(idObject.getId());
        RepositoryFile.Builder builder = new RepositoryFile.Builder(file);
        // fullName = title + extension
        String fullName;
        if (newTitle == null) {
            // keep existing file name
            fullName = file.getName();
        } else {
            // set new title
            builder.title(RepositoryFile.DEFAULT_LOCALE, newTitle)
                    // rename operation creates new revision, hence clear old value to be overwritten during saving
                    .createdDate(null);
            fullName = checkAndSanitize(newTitle) + objectType.getExtension();
        }

        String absPath = calcDestAbsPath(file, newDirectory, fullName);
        // get file from destination path, should be null for rename goal
        RepositoryFile fileFromDestination = pur.getFile(absPath);
        if (fileFromDestination == null) {
            file = builder.build();
            NodeRepositoryFileData data = pur.getDataAtVersionForRead(file.getId(), null,
                    NodeRepositoryFileData.class);
            if (newTitle != null) {
                // update file's content only if the title should be changed
                // as this action creates another revision
                pur.updateFile(file, data, versionComment);
            }
            pur.moveFile(idObject.getId(), absPath, null);
            rootRef.clearRef();
            return idObject;
        } else {
            throw new KettleException(BaseMessages.getString(PKG, errorMsgKey, file.getName(), newTitle));
        }
    }

    protected String getParentPath(final String path) {
        if (path == null) {
            throw new IllegalArgumentException();
        } else if (RepositoryFile.SEPARATOR.equals(path)) {
            return null;
        }
        int lastSlashIndex = path.lastIndexOf(RepositoryFile.SEPARATOR);
        if (lastSlashIndex == 0) {
            return RepositoryFile.SEPARATOR;
        } else if (lastSlashIndex > 0) {
            return path.substring(0, lastSlashIndex);
        } else {
            throw new IllegalArgumentException();
        }
    }

    protected String calcDestAbsPath(RepositoryFile existingFile, RepositoryDirectoryInterface newDirectory,
            String newName) {
        String newDirectoryPath = getPath(null, newDirectory, null);
        StringBuilder buf = new StringBuilder(existingFile.getPath().length());
        if (newDirectory != null) {
            buf.append(newDirectoryPath);
        } else {
            buf.append(getParentPath(existingFile.getPath()));
        }
        return buf.append(RepositoryFile.SEPARATOR).append(newName).toString();
    }

    @Override
    public void save(final RepositoryElementInterface element, final String versionComment,
            final ProgressMonitorListener monitor, final boolean overwriteAssociated) throws KettleException {
        save(element, versionComment, Calendar.getInstance(), monitor, overwriteAssociated);
    }

    @Override
    public void save(RepositoryElementInterface element, String versionComment, Calendar versionDate,
            ProgressMonitorListener monitor, boolean overwrite) throws KettleException {

        try {
            switch (element.getRepositoryElementType()) {
            case TRANSFORMATION:
                saveTrans(element, versionComment, versionDate);
                break;
            case JOB:
                saveJob(element, versionComment, versionDate);
                break;
            case DATABASE:
                saveDatabaseMeta(element, versionComment, versionDate);
                break;
            case SLAVE_SERVER:
                saveSlaveServer(element, versionComment, versionDate);
                break;
            case CLUSTER_SCHEMA:
                saveClusterSchema(element, versionComment, versionDate);
                break;
            case PARTITION_SCHEMA:
                savePartitionSchema(element, versionComment, versionDate);
                break;
            default:
                throw new KettleException(
                        "It's not possible to save Class [" + element.getClass().getName() + "] to the repository");
            }
        } catch (Exception e) {
            throw new KettleException("Unable to save repository element [" + element + "]", e);
        }
    }

    private boolean isRenamed(final RepositoryElementInterface element, final RepositoryFile file)
            throws KettleException {
        if (element.getObjectId() == null) {
            return false; // never been saved
        }
        String filename = element.getName();
        switch (element.getRepositoryElementType()) {
        case TRANSFORMATION:
            filename += RepositoryObjectType.TRANSFORMATION.getExtension();
            break;
        case JOB:
            filename += RepositoryObjectType.JOB.getExtension();
            break;
        case DATABASE:
            filename += RepositoryObjectType.DATABASE.getExtension();
            break;
        case SLAVE_SERVER:
            filename += RepositoryObjectType.SLAVE_SERVER.getExtension();
            break;
        case CLUSTER_SCHEMA:
            filename += RepositoryObjectType.CLUSTER_SCHEMA.getExtension();
            break;
        case PARTITION_SCHEMA:
            filename += RepositoryObjectType.PARTITION_SCHEMA.getExtension();
            break;
        default:
            throw new KettleException("unknown element type [" + element.getClass().getName() + "]");
        }
        if (!file.getName().equals(checkAndSanitize(filename))) {
            return true;
        }
        return false;
    }

    private void renameIfNecessary(final RepositoryElementInterface element, final RepositoryFile file)
            throws KettleException {
        if (!isRenamed(element, file)) {
            return;
        }

        // ObjectId id = element.getObjectId();
        StringBuilder buf = new StringBuilder(file.getPath().length());
        buf.append(getParentPath(file.getPath()));
        buf.append(RepositoryFile.SEPARATOR);
        buf.append(checkAndSanitize(element.getName()));
        switch (element.getRepositoryElementType()) {
        case DATABASE:
            buf.append(RepositoryObjectType.DATABASE.getExtension());
            break;
        case SLAVE_SERVER:
            buf.append(RepositoryObjectType.SLAVE_SERVER.getExtension());
            break;
        case CLUSTER_SCHEMA:
            buf.append(RepositoryObjectType.CLUSTER_SCHEMA.getExtension());
            break;
        case PARTITION_SCHEMA:
            buf.append(RepositoryObjectType.PARTITION_SCHEMA.getExtension());
            break;
        default:
            throw new KettleException(
                    "It's not possible to rename Class [" + element.getClass().getName() + "] to the repository");
        }
        pur.moveFile(file.getId(), buf.toString(), null);
    }

    /**
     * Use {@linkplain #saveKettleEntity} instead
     */
    @Deprecated
    protected void saveJob0(RepositoryElementInterface element, String versionComment, boolean saveSharedObjects,
            boolean checkLock, boolean checkRename, boolean loadRevision, boolean checkDeleted)
            throws KettleException {
        saveTransOrJob(jobDelegate, element, versionComment, null, saveSharedObjects, checkLock, checkRename,
                loadRevision, checkDeleted);
    }

    protected void saveJob(final RepositoryElementInterface element, final String versionComment,
            Calendar versionDate) throws KettleException {
        saveKettleEntity(element, versionComment, versionDate, true, true, true, true, true);
    }

    /**
     * Use {@linkplain #saveKettleEntity} instead
     */
    @Deprecated
    protected void saveTrans0(RepositoryElementInterface element, String versionComment, Calendar versionDate,
            boolean saveSharedObjects, boolean checkLock, boolean checkRename, boolean loadRevision,
            boolean checkDeleted) throws KettleException {
        saveTransOrJob(transDelegate, element, versionComment, versionDate, saveSharedObjects, checkLock,
                checkRename, loadRevision, checkDeleted);
    }

    protected boolean isDeleted(RepositoryFile file) {
        // no better solution so far
        return isInTrash(file);
    }

    protected boolean isInTrash(final RepositoryFile file) {
        // pretty hacky solution
        if (file.getPath().contains("/.trash/")) {
            return true;
        } else {
            return false;
        }
    }

    protected void saveTrans(final RepositoryElementInterface element, final String versionComment,
            Calendar versionDate) throws KettleException {
        saveKettleEntity(element, versionComment, versionDate, true, true, true, true, true);
    }

    protected void saveDatabaseMeta(final RepositoryElementInterface element, final String versionComment,
            Calendar versionDate) throws KettleException {
        try {
            // Even if the object id is null, we still have to check if the element is not present in the PUR
            // For example, if we import data from an XML file and there is a element with the same name in it.
            //
            if (element.getObjectId() == null) {
                element.setObjectId(getDatabaseID(element.getName()));
            }

            boolean isUpdate = element.getObjectId() != null;
            RepositoryFile file = null;
            if (isUpdate) {
                file = pur.getFileById(element.getObjectId().getId());

                // update title
                final String title = ((DatabaseMeta) element).getDisplayName();
                Date modifiedDate = null;
                if (versionDate != null && versionDate.getTime() != null) {
                    modifiedDate = versionDate.getTime();
                } else {
                    modifiedDate = new Date();
                }
                file = new RepositoryFile.Builder(file).title(RepositoryFile.DEFAULT_LOCALE, title)
                        .lastModificationDate(modifiedDate).build();
                renameIfNecessary(element, file);
                file = pur.updateFile(file,
                        new NodeRepositoryFileData(databaseMetaTransformer.elementToDataNode(element)),
                        versionComment);
            } else {
                Date createdDate = null;
                if (versionDate != null && versionDate.getTime() != null) {
                    createdDate = versionDate.getTime();
                } else {
                    createdDate = new Date();
                }
                file = new RepositoryFile.Builder(
                        checkAndSanitize(RepositoryFilenameUtils.escape(element.getName(), pur.getReservedChars())
                                + RepositoryObjectType.DATABASE.getExtension()))
                                        .title(RepositoryFile.DEFAULT_LOCALE, element.getName())
                                        .createdDate(createdDate).versioned(VERSION_SHARED_OBJECTS).build();
                file = pur.createFile(getDatabaseMetaParentFolderId(), file,
                        new NodeRepositoryFileData(databaseMetaTransformer.elementToDataNode(element)),
                        versionComment);
            }
            // side effects
            ObjectId objectId = new StringObjectId(file.getId().toString());
            element.setObjectId(objectId);
            element.setObjectRevision(getObjectRevision(objectId, null));
            if (element instanceof ChangedFlagInterface) {
                ((ChangedFlagInterface) element).clearChanged();
            }
            updateSharedObjectCache(element);
        } catch (Exception e) {
            // determine if there is an "access denied" issue and throw a nicer error message.
            if (e.getMessage().indexOf("access denied") >= 0) {
                throw new KettleException(BaseMessages.getString(PKG,
                        "PurRepository.ERROR_0004_DATABASE_UPDATE_ACCESS_DENIED", element.getName()), e);
            }
        }

    }

    @Override
    public DatabaseMeta loadDatabaseMeta(final ObjectId databaseId, final String versionId) throws KettleException {
        try {
            NodeRepositoryFileData data = pur.getDataAtVersionForRead(databaseId.getId(), versionId,
                    NodeRepositoryFileData.class);
            RepositoryFile file = null;
            if (versionId != null) {
                file = pur.getFileAtVersion(databaseId.getId(), versionId);
            } else {
                file = pur.getFileById(databaseId.getId());
            }
            return databaseMetaTransformer.assemble(file, data,
                    pur.getVersionSummary(databaseId.getId(), versionId));
        } catch (Exception e) {
            throw new KettleException("Unable to load database with id [" + databaseId + "]", e);
        }
    }

    @Override
    public TransMeta loadTransformation(final String transName, final RepositoryDirectoryInterface parentDir,
            final ProgressMonitorListener monitor, final boolean setInternalVariables, final String versionId)
            throws KettleException {
        String absPath = null;
        try {
            absPath = getPath(transName, parentDir, RepositoryObjectType.TRANSFORMATION);
            if (absPath == null) {
                // Couldn't resolve path, throw an exception
                throw new KettleFileException(BaseMessages.getString(PKG,
                        "PurRepository.ERROR_0002_TRANSFORMATION_NOT_FOUND", transName));
            }
            RepositoryFile file = pur.getFile(absPath);
            if (versionId != null) {
                // need to go back to server to get versioned info
                file = pur.getFileAtVersion(file.getId(), versionId);
            }
            NodeRepositoryFileData data = null;
            ObjectRevision revision = null;
            // Additional obfuscation through obscurity
            data = pur.getDataAtVersionForRead(file.getId(), versionId, NodeRepositoryFileData.class);
            revision = getObjectRevision(new StringObjectId(file.getId().toString()), versionId);
            TransMeta transMeta = buildTransMeta(file, parentDir, data, revision);
            ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.TransformationMetaLoaded.id,
                    transMeta);
            return transMeta;
        } catch (Exception e) {
            throw new KettleException("Unable to load transformation from path [" + absPath + "]", e);
        }
    }

    private TransMeta buildTransMeta(final RepositoryFile file, final RepositoryDirectoryInterface parentDir,
            final NodeRepositoryFileData data, final ObjectRevision revision) throws KettleException {
        TransMeta transMeta = new TransMeta();
        transMeta.setName(file.getTitle());
        transMeta.setDescription(file.getDescription());
        transMeta.setObjectId(new StringObjectId(file.getId().toString()));
        transMeta.setObjectRevision(revision);
        transMeta.setRepository(this);
        transMeta.setRepositoryDirectory(parentDir);
        transMeta.setMetaStore(getMetaStore());
        readTransSharedObjects(transMeta); // This should read from the local cache
        transDelegate.dataNodeToElement(data.getNode(), transMeta);
        transMeta.clearChanged();
        return transMeta;
    }

    /**
     * Load all transformations referenced by {@code files}.
     *
     * @param monitor
     * @param log
     * @param files
     *          Transformation files to load.
     * @param setInternalVariables
     *          Should internal variables be set when loading? (Note: THIS IS IGNORED, they are always set)
     * @return Loaded transformations
     * @throws KettleException
     *           Error loading data for transformations from repository
     */
    protected List<TransMeta> loadTransformations(final ProgressMonitorListener monitor,
            final LogChannelInterface log, final List<RepositoryFile> files, final boolean setInternalVariables)
            throws KettleException {
        List<TransMeta> transformations = new ArrayList<TransMeta>(files.size());
        List<NodeRepositoryFileData> filesData = pur.getDataForReadInBatch(files, NodeRepositoryFileData.class);
        List<VersionSummary> versions = pur.getVersionSummaryInBatch(files);
        Iterator<RepositoryFile> filesIter = files.iterator();
        Iterator<NodeRepositoryFileData> filesDataIter = filesData.iterator();
        Iterator<VersionSummary> versionsIter = versions.iterator();
        while ((monitor == null || !monitor.isCanceled()) && filesIter.hasNext()) {
            RepositoryFile file = filesIter.next();
            NodeRepositoryFileData fileData = filesDataIter.next();
            VersionSummary version = versionsIter.next();
            String dirPath = file.getPath().substring(0,
                    file.getPath().lastIndexOf(RepositoryDirectory.DIRECTORY_SEPARATOR));
            try {
                log.logDetailed("Loading/Exporting transformation [{0} : {1}]  ({2})", dirPath, file.getTitle(),
                        file.getPath()); //$NON-NLS-1$
                if (monitor != null) {
                    monitor.subTask("Exporting transformation [" + file.getPath() + "]"); //$NON-NLS-1$ //$NON-NLS-2$
                }
                TransMeta transMeta = buildTransMeta(file, findDirectory(dirPath), fileData,
                        createObjectRevision(version));
                ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.TransformationMetaLoaded.id,
                        transMeta);
                transformations.add(transMeta);
            } catch (Exception ex) {
                log.logDetailed("Unable to load transformation [" + file.getPath() + "]", ex); //$NON-NLS-1$ //$NON-NLS-2$
                log.logError("An error occurred reading transformation [" + file.getTitle() + "] from directory ["
                        + dirPath + "] : " + ex.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                log.logError("Transformation [" + file.getTitle() + "] from directory [" + dirPath
                        + "] was not exported because of a loading error!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
        }
        return transformations;
    }

    @Override
    public JobMeta loadJob(String jobname, RepositoryDirectoryInterface parentDir, ProgressMonitorListener monitor,
            String versionId) throws KettleException {
        String absPath = null;
        try {
            absPath = getPath(jobname, parentDir, RepositoryObjectType.JOB);
            if (absPath == null) {
                // Couldn't resolve path, throw an exception
                throw new KettleFileException(
                        BaseMessages.getString(PKG, "PurRepository.ERROR_0003_JOB_NOT_FOUND", jobname));
            }
            RepositoryFile file = pur.getFile(absPath);
            if (versionId != null) {
                // need to go back to server to get versioned info
                file = pur.getFileAtVersion(file.getId(), versionId);
            }
            NodeRepositoryFileData data = null;
            ObjectRevision revision = null;
            data = pur.getDataAtVersionForRead(file.getId(), versionId, NodeRepositoryFileData.class);
            revision = getObjectRevision(new StringObjectId(file.getId().toString()), versionId);
            JobMeta jobMeta = buildJobMeta(file, parentDir, data, revision);
            ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.JobMetaLoaded.id, jobMeta);
            return jobMeta;
        } catch (Exception e) {
            throw new KettleException("Unable to load transformation from path [" + absPath + "]", e);
        }
    }

    private JobMeta buildJobMeta(final RepositoryFile file, final RepositoryDirectoryInterface parentDir,
            final NodeRepositoryFileData data, final ObjectRevision revision) throws KettleException {
        JobMeta jobMeta = new JobMeta();
        jobMeta.setName(file.getTitle());
        jobMeta.setDescription(file.getDescription());
        jobMeta.setObjectId(new StringObjectId(file.getId().toString()));
        jobMeta.setObjectRevision(revision);
        jobMeta.setRepository(this);
        jobMeta.setRepositoryDirectory(parentDir);
        jobMeta.setMetaStore(getMetaStore());
        readJobMetaSharedObjects(jobMeta); // This should read from the local cache
        jobDelegate.dataNodeToElement(data.getNode(), jobMeta);
        jobMeta.clearChanged();
        return jobMeta;
    }

    /**
     * Load all jobs referenced by {@code files}.
     *
     * @param monitor
     * @param log
     * @param files
     *          Job files to load.
     * @param setInternalVariables
     *          Should internal variables be set when loading? (Note: THIS IS IGNORED, they are always set)
     * @return Loaded jobs
     * @throws KettleException
     *           Error loading data for jobs from repository
     */
    protected List<JobMeta> loadJobs(final ProgressMonitorListener monitor, final LogChannelInterface log,
            final List<RepositoryFile> files, final boolean setInternalVariables) throws KettleException {
        List<JobMeta> jobs = new ArrayList<JobMeta>(files.size());
        List<NodeRepositoryFileData> filesData = pur.getDataForReadInBatch(files, NodeRepositoryFileData.class);
        List<VersionSummary> versions = pur.getVersionSummaryInBatch(files);
        Iterator<RepositoryFile> filesIter = files.iterator();
        Iterator<NodeRepositoryFileData> filesDataIter = filesData.iterator();
        Iterator<VersionSummary> versionsIter = versions.iterator();
        while ((monitor == null || !monitor.isCanceled()) && filesIter.hasNext()) {
            RepositoryFile file = filesIter.next();
            NodeRepositoryFileData fileData = filesDataIter.next();
            VersionSummary version = versionsIter.next();
            try {
                String dirPath = file.getPath().substring(0,
                        file.getPath().lastIndexOf(RepositoryDirectory.DIRECTORY_SEPARATOR));
                log.logDetailed("Loading/Exporting job [{0} : {1}]  ({2})", dirPath, file.getTitle(),
                        file.getPath()); //$NON-NLS-1$
                if (monitor != null) {
                    monitor.subTask("Exporting job [" + file.getPath() + "]"); //$NON-NLS-1$ //$NON-NLS-2$
                }
                JobMeta jobMeta = buildJobMeta(file, findDirectory(dirPath), fileData,
                        createObjectRevision(version));
                ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.JobMetaLoaded.id, jobMeta);
                jobs.add(jobMeta);
            } catch (Exception ex) {
                log.logError("Unable to load job [" + file.getPath() + "]", ex); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }
        return jobs;

    }

    /**
     * Performs one-way conversion on incoming String to produce a syntactically valid JCR path (section 4.6 Path Syntax).
     */
    public static String checkAndSanitize(final String in) {
        if (in == null) {
            throw new IllegalArgumentException();
        }
        String extension = null;
        if (in.endsWith(RepositoryObjectType.CLUSTER_SCHEMA.getExtension())) {
            extension = RepositoryObjectType.CLUSTER_SCHEMA.getExtension();
        } else if (in.endsWith(RepositoryObjectType.DATABASE.getExtension())) {
            extension = RepositoryObjectType.DATABASE.getExtension();
        } else if (in.endsWith(RepositoryObjectType.JOB.getExtension())) {
            extension = RepositoryObjectType.JOB.getExtension();
        } else if (in.endsWith(RepositoryObjectType.PARTITION_SCHEMA.getExtension())) {
            extension = RepositoryObjectType.PARTITION_SCHEMA.getExtension();
        } else if (in.endsWith(RepositoryObjectType.SLAVE_SERVER.getExtension())) {
            extension = RepositoryObjectType.SLAVE_SERVER.getExtension();
        } else if (in.endsWith(RepositoryObjectType.TRANSFORMATION.getExtension())) {
            extension = RepositoryObjectType.TRANSFORMATION.getExtension();
        }
        String out = in;
        if (extension != null) {
            out = out.substring(0, out.length() - extension.length());
        }
        if (out.contains("/") || out.equals("..") || out.equals(".") || StringUtils.isBlank(out)) {
            throw new IllegalArgumentException();
        }
        if (System.getProperty("KETTLE_COMPATIBILITY_PUR_OLD_NAMING_MODE", "N").equals("Y")) {
            out = out.replaceAll("[/:\\[\\]\\*'\"\\|\\s\\.]", "_"); //$NON-NLS-1$//$NON-NLS-2$
        }
        if (extension != null) {
            return out + extension;
        } else {
            return out;
        }
    }

    protected void saveRepositoryElement(RepositoryElementInterface element, String versionComment,
            ITransformer transformer, Serializable elementsFolderId) throws KettleException {

        boolean isUpdate = (element.getObjectId() != null);
        RepositoryFile file;
        if (isUpdate) {
            file = pur.getFileById(element.getObjectId().getId());
            // update title & description
            file = new RepositoryFile.Builder(file).title(RepositoryFile.DEFAULT_LOCALE, element.getName())
                    .description(RepositoryFile.DEFAULT_LOCALE, Const.NVL(element.getDescription(), "")).build();

            // first rename, it is safe as only a name is changed, but not a path
            renameIfNecessary(element, file);
            file = pur.updateFile(file, new NodeRepositoryFileData(transformer.elementToDataNode(element)),
                    versionComment);
        } else {
            file = new RepositoryFile.Builder(
                    checkAndSanitize(element.getName() + element.getRepositoryElementType().getExtension()))
                            .title(RepositoryFile.DEFAULT_LOCALE, element.getName())
                            .description(RepositoryFile.DEFAULT_LOCALE, Const.NVL(element.getDescription(), ""))
                            .versioned(VERSION_SHARED_OBJECTS).build();
            file = pur.createFile(elementsFolderId, file,
                    new NodeRepositoryFileData(transformer.elementToDataNode(element)), versionComment);
        }
        // side effects
        ObjectId objectId = new StringObjectId(file.getId().toString());
        element.setObjectId(objectId);
        element.setObjectRevision(getObjectRevision(objectId, null));
        if (element instanceof ChangedFlagInterface) {
            ((ChangedFlagInterface) element).clearChanged();
        }
        updateSharedObjectCache(element);
    }

    protected void savePartitionSchema(final RepositoryElementInterface element, final String versionComment,
            Calendar versionDate) {
        try {
            // Even if the object id is null, we still have to check if the element is not present in the PUR
            // For example, if we import data from an XML file and there is a element with the same name in it.
            //
            if (element.getObjectId() == null) {
                element.setObjectId(getPartitionSchemaID(element.getName()));
            }

            saveRepositoryElement(element, versionComment, partitionSchemaTransformer,
                    getPartitionSchemaParentFolderId());
        } catch (KettleException ke) {
            ke.printStackTrace();
        }
    }

    protected void saveSlaveServer(final RepositoryElementInterface element, final String versionComment,
            Calendar versionDate) throws KettleException {
        try {
            // Even if the object id is null, we still have to check if the element is not present in the PUR
            // For example, if we import data from an XML file and there is a element with the same name in it.
            //
            if (element.getObjectId() == null) {
                element.setObjectId(getSlaveID(element.getName()));
            }

            saveRepositoryElement(element, versionComment, slaveTransformer, getSlaveServerParentFolderId());
        } catch (KettleException ke) {
            ke.printStackTrace();
        }
    }

    protected void saveClusterSchema(final RepositoryElementInterface element, final String versionComment,
            Calendar versionDate) {
        try {
            // Even if the object id is null, we still have to check if the element is not present in the PUR
            // For example, if we import data from an XML file and there is a element with the same name in it.
            //
            if (element.getObjectId() == null) {
                element.setObjectId(getClusterID(element.getName()));
            }

            saveRepositoryElement(element, versionComment, clusterTransformer, getClusterSchemaParentFolderId());
        } catch (KettleException ke) {
            ke.printStackTrace();
        }
    }

    private void updateSharedObjectCache(final RepositoryElementInterface element) throws KettleException {
        updateSharedObjectCache(element, null, null);
    }

    private void removeFromSharedObjectCache(final RepositoryObjectType type, final ObjectId id)
            throws KettleException {
        updateSharedObjectCache(null, type, id);
    }

    /**
     * Do not call this method directly. Instead call updateSharedObjectCache or removeFromSharedObjectCache.
     */
    private void updateSharedObjectCache(final RepositoryElementInterface element, final RepositoryObjectType type,
            final ObjectId id) throws KettleException {
        if (element != null && (element.getObjectId() == null || element.getObjectId().getId() == null)) {
            throw new IllegalArgumentException(element.getName() + " has a null id");
        }

        loadAndCacheSharedObjects(false);

        boolean remove = element == null;
        ObjectId idToFind = element != null ? element.getObjectId() : id;
        RepositoryObjectType typeToUpdate = element != null ? element.getRepositoryElementType() : type;
        RepositoryElementInterface elementToUpdate = null;
        List<? extends SharedObjectInterface> origSharedObjects = null;
        switch (typeToUpdate) {
        case DATABASE:
            origSharedObjects = sharedObjectsByType.get(RepositoryObjectType.DATABASE);
            if (!remove) {
                elementToUpdate = (RepositoryElementInterface) ((DatabaseMeta) element).clone();
            }
            break;
        case SLAVE_SERVER:
            origSharedObjects = sharedObjectsByType.get(RepositoryObjectType.SLAVE_SERVER);
            if (!remove) {
                elementToUpdate = (RepositoryElementInterface) ((SlaveServer) element).clone();
            }
            break;
        case CLUSTER_SCHEMA:
            origSharedObjects = sharedObjectsByType.get(RepositoryObjectType.CLUSTER_SCHEMA);
            if (!remove) {
                elementToUpdate = ((ClusterSchema) element).clone();
            }
            break;
        case PARTITION_SCHEMA:
            origSharedObjects = sharedObjectsByType.get(RepositoryObjectType.PARTITION_SCHEMA);
            if (!remove) {
                elementToUpdate = (RepositoryElementInterface) ((PartitionSchema) element).clone();
            }
            break;
        default:
            throw new KettleException("unknown type [" + typeToUpdate + "]");
        }
        List<SharedObjectInterface> newSharedObjects = new ArrayList<SharedObjectInterface>(origSharedObjects);
        // if there's a match on id, replace the element
        boolean found = false;
        for (int i = 0; i < origSharedObjects.size(); i++) {
            RepositoryElementInterface repositoryElementInterface = (RepositoryElementInterface) origSharedObjects
                    .get(i);
            if (repositoryElementInterface == null) {
                continue;
            }
            ObjectId objectId = repositoryElementInterface.getObjectId();
            if (objectId != null && objectId.equals(idToFind)) {
                if (remove) {
                    newSharedObjects.remove(i);
                } else {
                    elementToUpdate.setObjectId(idToFind); // because some clones don't clone the ID!!!
                    newSharedObjects.set(i, (SharedObjectInterface) elementToUpdate);
                }
                found = true;
            }
        }
        // otherwise, add it
        if (!remove && !found) {
            elementToUpdate.setObjectId(idToFind); // because some clones don't clone the ID!!!
            newSharedObjects.add((SharedObjectInterface) elementToUpdate);
        }
        sharedObjectsByType.put(typeToUpdate, newSharedObjects);
    }

    private ObjectRevision getObjectRevision(final ObjectId elementId, final String versionId) {
        return createObjectRevision(pur.getVersionSummary(elementId.getId(), versionId));
    }

    /**
     * @return Wrapped {@link VersionSummary} with a {@link ObjectRevision}.
     */
    protected ObjectRevision createObjectRevision(final VersionSummary versionSummary) {
        return new PurObjectRevision(versionSummary.getId(), versionSummary.getAuthor(), versionSummary.getDate(),
                versionSummary.getMessage());
    }

    private String getDatabaseMetaParentFolderPath() {
        return ClientRepositoryPaths.getEtcFolderPath() + RepositoryFile.SEPARATOR + FOLDER_PDI
                + RepositoryFile.SEPARATOR + FOLDER_DATABASES;
    }

    // package-local visibility for testing purposes
    Serializable getDatabaseMetaParentFolderId() {
        if (cachedDatabaseMetaParentFolderId == null) {
            RepositoryFile f = pur.getFile(getDatabaseMetaParentFolderPath());
            cachedDatabaseMetaParentFolderId = f.getId();
        }
        return cachedDatabaseMetaParentFolderId;
    }

    private String getPartitionSchemaParentFolderPath() {
        return ClientRepositoryPaths.getEtcFolderPath() + RepositoryFile.SEPARATOR + FOLDER_PDI
                + RepositoryFile.SEPARATOR + FOLDER_PARTITION_SCHEMAS;
    }

    private Serializable getPartitionSchemaParentFolderId() {
        if (cachedPartitionSchemaParentFolderId == null) {
            RepositoryFile f = pur.getFile(getPartitionSchemaParentFolderPath());
            cachedPartitionSchemaParentFolderId = f.getId();
        }
        return cachedPartitionSchemaParentFolderId;
    }

    private String getSlaveServerParentFolderPath() {
        return ClientRepositoryPaths.getEtcFolderPath() + RepositoryFile.SEPARATOR + FOLDER_PDI
                + RepositoryFile.SEPARATOR + FOLDER_SLAVE_SERVERS;
    }

    private Serializable getSlaveServerParentFolderId() {
        if (cachedSlaveServerParentFolderId == null) {
            RepositoryFile f = pur.getFile(getSlaveServerParentFolderPath());
            cachedSlaveServerParentFolderId = f.getId();
        }
        return cachedSlaveServerParentFolderId;
    }

    private String getClusterSchemaParentFolderPath() {
        return ClientRepositoryPaths.getEtcFolderPath() + RepositoryFile.SEPARATOR + FOLDER_PDI
                + RepositoryFile.SEPARATOR + FOLDER_CLUSTER_SCHEMAS;
    }

    private Serializable getClusterSchemaParentFolderId() {
        if (cachedClusterSchemaParentFolderId == null) {
            RepositoryFile f = pur.getFile(getClusterSchemaParentFolderPath());
            cachedClusterSchemaParentFolderId = f.getId();
        }
        return cachedClusterSchemaParentFolderId;
    }

    @Override
    public void saveConditionStepAttribute(ObjectId idTransformation, ObjectId idStep, String code,
            Condition condition) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveDatabaseMetaJobEntryAttribute(ObjectId idJob, ObjectId idJobentry, int nr, String nameCode,
            String idCode, DatabaseMeta database) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveDatabaseMetaStepAttribute(ObjectId idTransformation, ObjectId idStep, String code,
            DatabaseMeta database) throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveJobEntryAttribute(ObjectId idJob, ObjectId idJobentry, int nr, String code, String value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveJobEntryAttribute(ObjectId idJob, ObjectId idJobentry, int nr, String code, boolean value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveJobEntryAttribute(ObjectId idJob, ObjectId idJobentry, int nr, String code, long value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveStepAttribute(ObjectId idTransformation, ObjectId idStep, int nr, String code, String value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveStepAttribute(ObjectId idTransformation, ObjectId idStep, int nr, String code, boolean value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveStepAttribute(ObjectId idTransformation, ObjectId idStep, int nr, String code, long value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void saveStepAttribute(ObjectId idTransformation, ObjectId idStep, int nr, String code, double value)
            throws KettleException {
        // implemented by RepositoryProxy
        throw new UnsupportedOperationException();
    }

    @Override
    public void undeleteObject(final RepositoryElementMetaInterface element) throws KettleException {
        pur.undeleteFile(element.getObjectId().getId(), null);
        rootRef.clearRef();
    }

    @Override
    public List<RepositoryElementMetaInterface> getJobAndTransformationObjects(ObjectId id_directory,
            boolean includeDeleted) throws KettleException {
        return getPdiObjects(id_directory,
                Arrays.asList(RepositoryObjectType.JOB, RepositoryObjectType.TRANSFORMATION), includeDeleted);
    }

    @Override
    public IRepositoryService getService(Class<? extends IRepositoryService> clazz) throws KettleException {
        return purRepositoryServiceRegistry.getService(clazz);
    }

    @Override
    public List<Class<? extends IRepositoryService>> getServiceInterfaces() throws KettleException {
        return purRepositoryServiceRegistry.getRegisteredInterfaces();
    }

    @Override
    public boolean hasService(Class<? extends IRepositoryService> clazz) throws KettleException {
        return purRepositoryServiceRegistry.getService(clazz) != null;
    }

    @Override
    public RepositoryDirectoryInterface getDefaultSaveDirectory(RepositoryElementInterface repositoryElement)
            throws KettleException {
        return getUserHomeDirectory();
    }

    @Override
    public RepositoryDirectoryInterface getUserHomeDirectory() throws KettleException {
        return findDirectory(ClientRepositoryPaths.getUserHomeFolderPath(user.getLogin()));
    }

    @Override
    public RepositoryObject getObjectInformation(ObjectId objectId, RepositoryObjectType objectType)
            throws KettleException {
        try {
            RepositoryFile repositoryFile;
            try {
                repositoryFile = pur.getFileById(objectId.getId());
            } catch (Exception e) {
                // javax.jcr.Session throws exception, if a node with specified ID does not exist
                // see http://jira.pentaho.com/browse/BISERVER-12758
                log.logError("Error when trying to obtain a file by id: " + objectId.getId(), e);
                return null;
            }
            if (repositoryFile == null) {
                return null;
            }

            RepositoryFileAcl repositoryFileAcl = pur.getAcl(repositoryFile.getId());
            String parentPath = getParentPath(repositoryFile.getPath());
            String name = repositoryFile.getTitle();
            String description = repositoryFile.getDescription();
            Date modifiedDate = repositoryFile.getLastModifiedDate();
            // String creatorId = repositoryFile.getCreatorId();
            String ownerName = repositoryFileAcl != null ? repositoryFileAcl.getOwner().getName() : "";
            boolean deleted = isDeleted(repositoryFile);
            RepositoryDirectoryInterface directory = findDirectory(parentPath);
            return new RepositoryObject(objectId, name, directory, ownerName, modifiedDate, objectType, description,
                    deleted);
        } catch (Exception e) {
            throw new KettleException("Unable to get object information for object with id=" + objectId, e);
        }
    }

    @Override
    public RepositoryDirectoryInterface findDirectory(String directory) throws KettleException {
        RepositoryDirectoryInterface repositoryDirectoryInterface = null;
        // check if we have a rootRef cached
        boolean usingRootDirCache = rootRef.getRef() != null;
        repositoryDirectoryInterface = getRootDir().findDirectory(directory);
        // if we are using a cached version of the repository interface, allow a reload if we do not find
        if (repositoryDirectoryInterface == null && usingRootDirCache) {
            repositoryDirectoryInterface = loadRepositoryDirectoryTree().findDirectory(directory);
        }
        return repositoryDirectoryInterface;
    }

    @Override
    public RepositoryDirectoryInterface findDirectory(ObjectId directory) throws KettleException {
        RepositoryDirectoryInterface repositoryDirectoryInterface = null;
        // check if we have a rootRef cached
        boolean usingRootDirCache = rootRef.getRef() != null;
        repositoryDirectoryInterface = getRootDir().findDirectory(directory);
        // if we are using a cached version of the repository interface, allow a reload if we do not find
        if (repositoryDirectoryInterface == null && usingRootDirCache) {
            repositoryDirectoryInterface = loadRepositoryDirectoryTree().findDirectory(directory);
        }
        return repositoryDirectoryInterface;
    }

    @Override
    public JobMeta loadJob(ObjectId idJob, String versionLabel) throws KettleException {
        try {
            RepositoryFile file = null;
            if (versionLabel != null) {
                file = pur.getFileAtVersion(idJob.getId(), versionLabel);
            } else {
                file = pur.getFileById(idJob.getId());
            }
            EEJobMeta jobMeta = new EEJobMeta();
            jobMeta.setName(file.getTitle());
            jobMeta.setDescription(file.getDescription());
            jobMeta.setObjectId(new StringObjectId(file.getId().toString()));
            jobMeta.setObjectRevision(getObjectRevision(new StringObjectId(file.getId().toString()), versionLabel));
            jobMeta.setRepository(this);
            jobMeta.setRepositoryDirectory(findDirectory(getParentPath(file.getPath())));

            jobMeta.setMetaStore(getMetaStore()); // inject metastore

            readJobMetaSharedObjects(jobMeta);
            // Additional obfuscation through obscurity
            jobMeta.setRepositoryLock(unifiedRepositoryLockService.getLock(file));
            jobDelegate.dataNodeToElement(pur
                    .getDataAtVersionForRead(idJob.getId(), versionLabel, NodeRepositoryFileData.class).getNode(),
                    jobMeta);

            ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.JobMetaLoaded.id, jobMeta);

            jobMeta.clearChanged();
            return jobMeta;
        } catch (Exception e) {
            throw new KettleException("Unable to load job with id [" + idJob + "]", e);
        }
    }

    @Override
    public TransMeta loadTransformation(ObjectId idTransformation, String versionLabel) throws KettleException {
        try {
            RepositoryFile file = null;
            if (versionLabel != null) {
                file = pur.getFileAtVersion(idTransformation.getId(), versionLabel);
            } else {
                file = pur.getFileById(idTransformation.getId());
            }
            EETransMeta transMeta = new EETransMeta();
            transMeta.setName(file.getTitle());
            transMeta.setDescription(file.getDescription());
            transMeta.setObjectId(new StringObjectId(file.getId().toString()));
            transMeta.setObjectRevision(
                    getObjectRevision(new StringObjectId(file.getId().toString()), versionLabel));
            transMeta.setRepository(this);
            transMeta.setRepositoryDirectory(findDirectory(getParentPath(file.getPath())));
            transMeta.setRepositoryLock(unifiedRepositoryLockService.getLock(file));
            transMeta.setMetaStore(getMetaStore()); // inject metastore

            readTransSharedObjects(transMeta);
            transDelegate.dataNodeToElement(pur
                    .getDataAtVersionForRead(idTransformation.getId(), versionLabel, NodeRepositoryFileData.class)
                    .getNode(), transMeta);

            ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.TransformationMetaLoaded.id,
                    transMeta);

            transMeta.clearChanged();
            return transMeta;
        } catch (Exception e) {
            throw new KettleException("Unable to load transformation with id [" + idTransformation + "]", e);
        }
    }

    @Override
    public String getConnectMessage() {
        return connectMessage;
    }

    @Override
    public String[] getJobsUsingDatabase(ObjectId id_database) throws KettleException {
        List<String> result = new ArrayList<String>();
        for (RepositoryFile file : getReferrers(id_database, Collections.singletonList(RepositoryObjectType.JOB))) {
            result.add(file.getPath());
        }
        return result.toArray(new String[result.size()]);
    }

    @Override
    public String[] getTransformationsUsingDatabase(ObjectId id_database) throws KettleException {
        List<String> result = new ArrayList<String>();
        for (RepositoryFile file : getReferrers(id_database,
                Collections.singletonList(RepositoryObjectType.TRANSFORMATION))) {
            result.add(file.getPath());
        }
        return result.toArray(new String[result.size()]);
    }

    protected List<RepositoryFile> getReferrers(ObjectId fileId, List<RepositoryObjectType> referrerTypes)
            throws KettleException {
        // Use a result list to append to; Removing from the files list was causing a concurrency exception
        List<RepositoryFile> result = new ArrayList<RepositoryFile>();
        List<RepositoryFile> files = pur.getReferrers(fileId.getId());

        // Filter out types
        if (referrerTypes != null && referrerTypes.size() > 0) {
            for (RepositoryFile file : files) {
                if (referrerTypes.contains(getObjectType(file.getName()))) {
                    result.add(file);
                }
            }
        }
        return result;
    }

    @Override
    public IRepositoryExporter getExporter() throws KettleException {
        final List<String> exportPerms = Arrays.asList(IAbsSecurityProvider.CREATE_CONTENT_ACTION,
                IAbsSecurityProvider.EXECUTE_CONTENT_ACTION);
        IAbsSecurityProvider securityProvider = purRepositoryServiceRegistry.getService(IAbsSecurityProvider.class);
        StringBuilder errorMessage = new StringBuilder("[");
        for (String perm : exportPerms) {
            if (securityProvider == null && PurRepositoryConnector.inProcess()) {
                return new PurRepositoryExporter(this);
            }
            if (securityProvider != null && securityProvider.isAllowed(perm)) {
                return new PurRepositoryExporter(this);
            }
            errorMessage.append(perm);
            errorMessage.append(", ");
        }
        errorMessage.setLength(errorMessage.length() - 2);
        errorMessage.append("]");

        throw new KettleSecurityException(BaseMessages.getString(PKG,
                "PurRepository.ERROR_0005_INCORRECT_PERMISSION", errorMessage.toString()));
    }

    @Override
    public IRepositoryImporter getImporter() {
        return new PurRepositoryImporter(this);
    }

    public IUnifiedRepository getPur() {
        return pur;
    }

    @Override
    public IMetaStore getMetaStore() {
        return metaStore;
    }

    public ServiceManager getServiceManager() {
        return purRepositoryConnector == null ? null : purRepositoryConnector.getServiceManager();
    }

    /**
     * Saves {@code element} in repository. {@code element} show represent either a transformation or a job. <br/>
     * The method throws {@code KettleException} in the following cases:
     * <ul>
     *   <li>{@code element} is not a {@linkplain TransMeta} or {@linkplain JobMeta}</li>
     *   <li>{@code checkLock == true} and the file is locked and cannot be unlocked</li>
     *   <li>{@code checkDeleted == true} and the file was removed</li>
     *   <li>{@code checkRename == true} and the file was renamed and renaming failed</li>
     * </ul>
     *
     * @param element
     *          job or transformation
     * @param versionComment
     *          revision comment
     * @param versionDate
     *          revision timestamp
     * @param saveSharedObjects
     *          flag of saving element's shared objects
     * @param checkLock
     *          flag of checking whether the corresponding file is locked
     * @param checkRename
     *          flag of checking whether it is necessary to rename the file
     * @param loadRevision
     *          flag of setting element's revision
     * @param checkDeleted
     *          flag of checking whether the file was deleted
     * @throws KettleException
     *           if any of aforementioned conditions is {@code true}
     */
    protected void saveKettleEntity(RepositoryElementInterface element, String versionComment, Calendar versionDate,
            boolean saveSharedObjects, boolean checkLock, boolean checkRename, boolean loadRevision,
            boolean checkDeleted) throws KettleException {
        ISharedObjectsTransformer objectTransformer;
        switch (element.getRepositoryElementType()) {
        case TRANSFORMATION:
            objectTransformer = transDelegate;
            break;
        case JOB:
            objectTransformer = jobDelegate;
            break;
        default:
            throw new KettleException("Unknown RepositoryObjectType. Should be TRANSFORMATION or JOB ");
        }
        saveTransOrJob(objectTransformer, element, versionComment, versionDate, saveSharedObjects, checkLock,
                checkRename, loadRevision, checkDeleted);
    }

    protected void saveTransOrJob(ISharedObjectsTransformer objectTransformer, RepositoryElementInterface element,
            String versionComment, Calendar versionDate, boolean saveSharedObjects, boolean checkLock,
            boolean checkRename, boolean loadRevision, boolean checkDeleted) throws KettleException {
        if (saveSharedObjects) {
            objectTransformer.saveSharedObjects(element, versionComment);
        }

        final boolean isUpdate = (element.getObjectId() != null);
        RepositoryFile file;
        if (isUpdate) {
            ObjectId id = element.getObjectId();
            file = pur.getFileById(id.getId());
            if (checkLock && file.isLocked() && !unifiedRepositoryLockService.canUnlockFileById(id)) {
                throw new KettleException("File is currently locked by another user for editing");
            }
            if (checkDeleted && isInTrash(file)) {
                // absolutely awful to have UI references in this class :(
                throw new KettleException("File is in the Trash. Use Save As.");
            }
            // update title and description
            file = new RepositoryFile.Builder(file).title(RepositoryFile.DEFAULT_LOCALE, element.getName())
                    .createdDate(versionDate != null ? versionDate.getTime() : new Date())
                    .description(RepositoryFile.DEFAULT_LOCALE, Const.NVL(element.getDescription(), "")).build();
            file = pur.updateFile(file, new NodeRepositoryFileData(objectTransformer.elementToDataNode(element)),
                    versionComment);
            if (checkRename && isRenamed(element, file)) {
                renameKettleEntity(element, null, element.getName());
            }
        } else {
            file = new RepositoryFile.Builder(
                    checkAndSanitize(element.getName() + element.getRepositoryElementType().getExtension()))
                            .versioned(true).title(RepositoryFile.DEFAULT_LOCALE, element.getName())
                            .createdDate(versionDate != null ? versionDate.getTime() : new Date())
                            .description(RepositoryFile.DEFAULT_LOCALE, Const.NVL(element.getDescription(), ""))
                            .build();
            file = pur.createFile(element.getRepositoryDirectory().getObjectId().getId(), file,
                    new NodeRepositoryFileData(objectTransformer.elementToDataNode(element)), versionComment);
        }
        // side effects
        ObjectId objectId = new StringObjectId(file.getId().toString());
        element.setObjectId(objectId);
        if (loadRevision) {
            element.setObjectRevision(getObjectRevision(objectId, null));
        }
        if (element instanceof ChangedFlagInterface) {
            ((ChangedFlagInterface) element).clearChanged();
        }

        if (element.getRepositoryElementType() == RepositoryObjectType.TRANSFORMATION) {
            TransMeta transMeta = loadTransformation(objectId, null);
            ExtensionPointHandler.callExtensionPoint(log, KettleExtensionPoint.TransImportAfterSaveToRepo.id,
                    transMeta);
        }
    }

    protected ObjectId renameKettleEntity(final RepositoryElementInterface transOrJob,
            final RepositoryDirectoryInterface newDirectory, final String newName) throws KettleException {
        switch (transOrJob.getRepositoryElementType()) {
        case TRANSFORMATION:
            return renameTransformation(transOrJob.getObjectId(), null, newDirectory, newName);
        case JOB:
            return renameJob(transOrJob.getObjectId(), null, newDirectory, newName);
        default:
            throw new KettleException("Unknown RepositoryObjectType. Should be TRANSFORMATION or JOB ");
        }
    }

    @Override
    public boolean test() {
        String repoUrl = repositoryMeta.getRepositoryLocation().getUrl();
        final String url = repoUrl + (repoUrl.endsWith("/") ? "" : "/") + "webservices/unifiedRepository?wsdl";
        Service service;
        try {
            service = Service.create(new URL(url), new QName("http://www.pentaho.org/ws/1.0", "unifiedRepository"));
            if (service != null) {
                IUnifiedRepositoryJaxwsWebService repoWebService = service
                        .getPort(IUnifiedRepositoryJaxwsWebService.class);
                if (repoWebService != null) {
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    private RepositoryFileTree loadRepositoryFileTreeFolders(String path, int depth, boolean includeAcls,
            boolean showHidden) {
        RepositoryRequest repoRequest = new RepositoryRequest();
        repoRequest.setDepth(depth);
        repoRequest.setIncludeAcls(includeAcls);
        repoRequest.setChildNodeFilter("*");
        repoRequest.setTypes(FILES_TYPE_FILTER.FOLDERS);
        repoRequest.setPath(path);
        repoRequest.setShowHidden(showHidden);
        return pur.getTree(repoRequest);
    }
}