/******************************************************************************* * Copyright (c) 2012 Arapiki Solutions Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * psmith - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.model.impl; import java.io.File; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import com.buildml.model.FatalBuildStoreError; import com.buildml.model.IBuildStore; import com.buildml.model.IFileMgr; import com.buildml.model.IPackageMemberMgr; import com.buildml.model.IPackageMgr; import com.buildml.model.IPackageMgrListener; import com.buildml.model.IPackageRootMgr; import com.buildml.model.IFileMgr.PathType; import com.buildml.model.types.FileSet; import com.buildml.utils.errors.ErrorCode; /** * A {@link PackageRootMgr} object manages the mapping o * * @author Peter Smith <psmith@arapiki.com> */ public class PackageRootMgr implements IPackageRootMgr { /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** The IBuildStore object that owns this manager */ private IBuildStore buildStore; /** The associated FileMgr */ private IFileMgr fileMgr; /** The associated PackageMgr */ private IPackageMgr pkgMgr; /** The associated PackageMemberMgr */ private IPackageMemberMgr pkgMemberMgr; /** * Our database manager object, used to access the database content. This is provided * to us when the PackageRootMgr object is first instantiated. */ private BuildStoreDB db = null; /** * We cache the pathID of the workspace root, to save accessing the database too often. */ private int cachedWorkspaceRootId = -1; /** * The cached version of the native workspace root. */ private String cachedWorkspaceRootNative = null; /** * The map of root names to native paths. The format of the key is "<pkgId>_<rootType>". * For example, "34_1". */ private HashMap<String, String> nativeRootMap = null; /** * Various prepared statements for database access. */ private PreparedStatement getWorkspaceDistancePrepStmt = null, setWorkspaceDistancePrepStmt = null, findRootPathIdPrepStmt = null, insertRootPrepStmt = null, updateRootPathPrepStmt = null, findRootNamesPrepStmt = null, findRootNamesAtPathPrepStmt = null, deleteFileRootPrepStmt = null; /** The event listeners who are registered to learn about package changes */ List<IPackageMgrListener> listeners = new ArrayList<IPackageMgrListener>(); /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new PackageRootMgr. * * @param buildStore The BuildStore that this PackageRootMgr object belongs to. */ public PackageRootMgr(BuildStore buildStore) { this.buildStore = buildStore; this.fileMgr = buildStore.getFileMgr(); this.pkgMgr = buildStore.getPackageMgr(); this.pkgMemberMgr = buildStore.getPackageMemberMgr(); this.db = buildStore.getBuildStoreDB(); /* initialize map of root -> native path mappings */ nativeRootMap = new HashMap<String, String>(); /* initialize prepared database statements */ getWorkspaceDistancePrepStmt = db.prepareStatement("select distance from workspace"); setWorkspaceDistancePrepStmt = db.prepareStatement("update workspace set distance = ?"); findRootPathIdPrepStmt = db.prepareStatement("select fileId from fileRoots where name = ?"); insertRootPrepStmt = db.prepareStatement("insert into fileRoots values (?, ?)"); updateRootPathPrepStmt = db.prepareStatement("update fileRoots set fileId = ? where name = ?"); findRootNamesPrepStmt = db.prepareStatement("select name from fileRoots order by name"); findRootNamesAtPathPrepStmt = db.prepareStatement("select name from fileRoots where fileId = ? order by name"); deleteFileRootPrepStmt = db.prepareStatement("delete from fileRoots where name = ?"); } /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#setWorkspaceRoot(int) */ @Override public int setWorkspaceRoot(int workspacePathId) { /* first, validate that this root is valid */ PathType pathType = fileMgr.getPathType(workspacePathId); if ((pathType == PathType.TYPE_INVALID) || (fileMgr.isPathTrashed(workspacePathId))) { return ErrorCode.BAD_PATH; } else if (pathType != PathType.TYPE_DIR) { return ErrorCode.NOT_A_DIRECTORY; } /* * Verify that all existing package roots are at the same level, or below the * new proposed workspace root. */ String [] roots = getRoots(); for (int i = 0; i < roots.length; i++) { String rootName = roots[i]; if (!rootName.equals("root") && !rootName.equals("workspace")) { int rootPathId = getRootPath(rootName); if ((rootPathId != workspacePathId) && !fileMgr.isAncestorOf(workspacePathId, rootPathId)) { return ErrorCode.OUT_OF_RANGE; } } } int rc = addRoot("workspace", workspacePathId); if (rc != ErrorCode.OK) { return rc; } /* cache the value for performance reasons */ cachedWorkspaceRootId = workspacePathId; return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getWorkspaceRoot() */ @Override public int getWorkspaceRoot() { /* If we don't have a cached copy, query the database */ if (cachedWorkspaceRootId == -1) { Integer results[] = null; try { findRootPathIdPrepStmt.setString(1, "workspace"); results = db.executePrepSelectIntegerColumn(findRootPathIdPrepStmt); /* is there exactly one root registered? */ if (results.length == 1) { cachedWorkspaceRootId = results[0]; } /* else, we didn't find the root */ else { return ErrorCode.NOT_FOUND; } } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } } return cachedWorkspaceRootId; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#setWorkspaceRootNative(java.lang.String) */ @Override public int setWorkspaceRootNative(String nativePath) { /* check that this in a valid native directory */ File dirFile = new File(nativePath); if (!dirFile.isDirectory()) { return ErrorCode.NOT_A_DIRECTORY; } /* this path is only stored locally - not persisted in the database */ cachedWorkspaceRootNative = dirFile.toString(); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getWorkspaceRootNative() */ @Override public String getWorkspaceRootNative() { /* * If there's no cached native workspace root (nobody has done a setWorkspaceRootNative() * or a setBuildMLDepth()), compute it from the persisted "depth" */ if (cachedWorkspaceRootNative == null) { Integer results[] = db.executePrepSelectIntegerColumn(getWorkspaceDistancePrepStmt); if (results.length == 1) { setBuildMLFileDepth(results[0]); } else { throw new FatalBuildStoreError( "Unable to determine build.bml file depth in workspace"); } } return cachedWorkspaceRootNative; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#setRelativeWorkspace(int) */ @Override public int setBuildMLFileDepth(int depth) { if (depth < 0) { return ErrorCode.BAD_PATH; } /* determine the path to our BuildML file, and move upwards "depth" levels. */ String dbFileName = db.getDatabaseFileName(); File dbFile = new File(dbFileName); for (int i = 0; i <= depth; i++) { dbFile = dbFile.getParentFile(); if (dbFile == null) { return ErrorCode.BAD_PATH; } } /* persistent the depth value into the database */ try { setWorkspaceDistancePrepStmt.setInt(1, depth); db.executePrepUpdate(setWorkspaceDistancePrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } /* * Refresh the cache with a new copy of the native workspace root. Note that * often the suffix /. is added to the file name (probably because of the getParentFile() * operation above. We strip this off, since it's unattractive. */ cachedWorkspaceRootNative = dbFile.toString(); if (cachedWorkspaceRootNative.endsWith("/.")) { cachedWorkspaceRootNative = cachedWorkspaceRootNative.substring(0, cachedWorkspaceRootNative.length() - 2); } return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#setPackageRoot(int, int, java.lang.String) */ @Override public int setPackageRoot(int packageId, int type, int rootPathId) { /* check that pathId is valid (not trashed) */ if (fileMgr.isPathTrashed(rootPathId)) { return ErrorCode.BAD_PATH; } /* check that pathId is a directory */ if (fileMgr.getPathType(rootPathId) != PathType.TYPE_DIR) { return ErrorCode.NOT_A_DIRECTORY; } /* check that pathId is below @workspace */ int workspaceRootId = getWorkspaceRoot(); if (workspaceRootId == ErrorCode.NOT_FOUND) { return ErrorCode.NOT_FOUND; } if ((workspaceRootId != rootPathId) && (!fileMgr.isAncestorOf(workspaceRootId, rootPathId))) { return ErrorCode.OUT_OF_RANGE; } /* we can't modify the <import> package */ if (packageId == pkgMgr.getImportPackage()) { return ErrorCode.NOT_FOUND; } /* test that all paths in the package are below the new proposed package root */ FileSet memberPaths = pkgMemberMgr.getFilesInPackage(packageId); for (int memberPathId : memberPaths) { if ((memberPathId != rootPathId) && (!fileMgr.isAncestorOf(rootPathId, memberPathId))) { return ErrorCode.OUT_OF_RANGE; } } /* derive the root name, e.g <package>_src or <package>_gen */ String rootName = getPackageRootName(packageId, type); if (rootName == null) { return ErrorCode.NOT_FOUND; } /* fetch the existing root - we need to know if it changes before we notify */ int existingRootPathId = getPackageRoot(packageId, type); int rc = addRoot(rootName, rootPathId); /* if the root actually changed, notify listeners */ if (existingRootPathId != rootPathId) { notifyListeners(packageId, IPackageMgrListener.CHANGED_ROOTS); } return rc; } /*-------------------------------------------------------------------------------------*/ /* * (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getPackageRoot(int, int) */ @Override public int getPackageRoot(int packageId, int type) { String rootName = getPackageRootName(packageId, type); if (rootName == null) { return ErrorCode.NOT_FOUND; } return getRootPath(rootName); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#removePackageRoot(int, int) */ @Override public int removePackageRoot(int packageId, int type) { String rootName = getPackageRootName(packageId, type); if (rootName == null) { return ErrorCode.NOT_FOUND; } try { deleteFileRootPrepStmt.setString(1, rootName); int rowCount = db.executePrepUpdate(deleteFileRootPrepStmt); if (rowCount == 0) { return ErrorCode.NOT_FOUND; } } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* * (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getPackageRootName(int, int) */ @Override public String getPackageRootName(int packageId, int type) { /* Not supported for folders, invalid packages or <import> */ if (pkgMgr.isFolder(packageId) || !pkgMgr.isValid(packageId) || (packageId == pkgMgr.getImportPackage())) { return null; } /* get the package name, and append _src or _gen */ String packageName = pkgMgr.getName(packageId); if (packageName == null) { return null; } if (type == IPackageRootMgr.SOURCE_ROOT) { return packageName + "_src"; } else if (type == IPackageRootMgr.GENERATED_ROOT) { return packageName + "_gen"; } return null; } /*-------------------------------------------------------------------------------------*/ /** * Given a root name, return the associated path ID. * * @param rootName Name of the root to search for. * @return The pathId associated with the root, or ErrorCode.NOT_FOUND if the root name * is invalid. */ public int getRootPath(String rootName) { try { findRootPathIdPrepStmt.setString(1, rootName); Integer results[] = db.executePrepSelectIntegerColumn(findRootPathIdPrepStmt); /* is there exactly one root registered? */ if (results.length == 1) { return results[0]; } } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } return ErrorCode.NOT_FOUND; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#overridePackageRoot(int, int, java.lang.String) */ @Override public int setPackageRootNative(int packageId, int type, String path) { /* validate the package ID */ String pkgName = pkgMgr.getName(packageId); if (pkgName == null){ return ErrorCode.NOT_FOUND; } /* validate the package type */ if ((type != IPackageRootMgr.SOURCE_ROOT) && (type != IPackageRootMgr.GENERATED_ROOT)) { return ErrorCode.BAD_VALUE; } /* paths must be absolute */ if (!path.startsWith("/")) { return ErrorCode.BAD_PATH; } /* validate that paths refer to a real native directory */ File nativePath = new File(path); if (!nativePath.exists() || !nativePath.isDirectory()) { return ErrorCode.BAD_PATH; } nativeRootMap.put(packageId + "_" + type, path); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#clearPackageRoot(int, int) */ @Override public int clearPackageRootNative(int packageId, int type) { /* validate the package ID */ String pkgName = pkgMgr.getName(packageId); if (pkgName == null){ return ErrorCode.NOT_FOUND; } /* validate the package type */ if ((type != IPackageRootMgr.SOURCE_ROOT) && (type != IPackageRootMgr.GENERATED_ROOT)) { return ErrorCode.BAD_VALUE; } /* remove the native path mapping (which may or may not already exist) */ nativeRootMap.remove(packageId + "_" + type); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getPackageRoot(int, int) */ @Override public String getPackageRootNative(int packageId, int type) { /* is there a native path override? */ String override = nativeRootMap.get(packageId + "_" + type); if (override != null) { return override; } /* * No override - compute relative path from workspace to root, and append to * the native path of the workspace. */ int wsRootId = getWorkspaceRoot(); int rootId = getPackageRoot(packageId, type); if (rootId == ErrorCode.NOT_FOUND) { return null; } String wsRootPath = fileMgr.getPathName(wsRootId); String rootPath = fileMgr.getPathName(rootId); String relativePath = rootPath.substring(wsRootPath.length(), rootPath.length()); return getWorkspaceRootNative() + relativePath; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getPackageRoot(java.lang.String) */ @Override public String getRootNative(String rootName) { if (rootName.equals("root")) { return "/"; } else if (rootName.equals("workspace")) { return getWorkspaceRootNative(); } else { /* determine type of root, based on suffix */ int packageType; if (rootName.endsWith("_src")) { packageType = IPackageRootMgr.SOURCE_ROOT; } else if (rootName.endsWith("_gen")) { packageType = IPackageRootMgr.GENERATED_ROOT; } else { return null; } /* find package ID number */ String packageName = rootName.substring(0, rootName.length() - 4); int pkgId = pkgMgr.getId(packageName); if (pkgId == ErrorCode.NOT_FOUND) { return null; } return getPackageRootNative(pkgId, packageType); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getRoots() */ @Override public String[] getRoots() { return db.executePrepSelectStringColumn(findRootNamesPrepStmt); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageRootMgr#getRootAtPath(int) */ @Override public String[] getRootsAtPath(int pathId) { /* fetch all records at this path */ try { findRootNamesAtPathPrepStmt.setInt(1, pathId); return db.executePrepSelectStringColumn(findRootNamesAtPathPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageMgr#addListener(com.buildml.model.IPackageMgrListener) */ @Override public void addListener(IPackageMgrListener listener) { listeners.add(listener); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IPackageMgr#removeListener(com.buildml.model.IPackageMgrListener) */ @Override public void removeListener(IPackageMgrListener listener) { listeners.remove(listener); }; /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * @param rootName Name of the root to be added (updated). * @param pathId The ID of the path to attach the root to. * @return ErrorCode.OK on success, ErrorCode.BAD_PATH if the pathId is invalid, * ErrorCode.NOT_A_DIRECTORY if the pathId doesn't reference a directory. */ private int addRoot(String rootName, int pathId) { /* the path must not be trashed */ if (fileMgr.isPathTrashed(pathId)) { return ErrorCode.BAD_PATH; } /* this path must be valid, and refer to a directory, not a file */ PathType pathType = fileMgr.getPathType(pathId); if (pathType == PathType.TYPE_INVALID) { return ErrorCode.BAD_PATH; } if (pathType != PathType.TYPE_DIR){ return ErrorCode.NOT_A_DIRECTORY; } /* start by trying to update the existing "workspace" record */ try { updateRootPathPrepStmt.setInt(1, pathId); updateRootPathPrepStmt.setString(2, rootName); if (db.executePrepUpdate(updateRootPathPrepStmt) == 0) { /* doesn't exist yet, insert it */ insertRootPrepStmt.setString(1, rootName); insertRootPrepStmt.setInt(2, pathId); db.executePrepUpdate(insertRootPrepStmt); } } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /** * Notify any registered listeners about our change in state. * @param pkgId The package that has changed. * @param how The way in which the package changed (see {@link IPackageMgrListener}). */ private void notifyListeners(int pkgId, int how) { /* * Make a copy of the listeners list, otherwise a registered listener can't remove * itself from the list within the packageChangeNotification() method. */ IPackageMgrListener listenerCopy[] = listeners.toArray(new IPackageMgrListener[listeners.size()]); for (int i = 0; i < listenerCopy.length; i++) { listenerCopy[i].packageChangeNotification(pkgId, how); } } /*-------------------------------------------------------------------------------------*/ }