/******************************************************************************* * Copyright (c) 2014 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: * "Peter Smith <psmith@arapiki.com>" - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.model.impl; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import com.buildml.model.FatalBuildStoreError; import com.buildml.model.IBuildStore; import com.buildml.model.IPackageMemberMgr; import com.buildml.model.IPackageMgr; import com.buildml.model.ISlotTypes; import com.buildml.model.ISubPackageMgr; import com.buildml.model.ISubPackageMgrListener; import com.buildml.model.ISlotTypes.SlotDetails; import com.buildml.utils.errors.ErrorCode; /** * A manager class (that supports the BuildStore class) responsible for managing all * BuildStore information pertaining to sub-packages. * <p> * There should be exactly one SubPackageMgr object per BuildStore object. Use the * BuildStore's getSubPackageMgr() method to obtain that one instance. * * @author "Peter Smith <psmith@arapiki.com>" */ /* package private */ class SubPackageMgr implements ISubPackageMgr { /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** The BuildStore object that owns all the manager objects. */ private IBuildStore buildStore = null; /** * Our database manager object, used to access the database content. This is provided * to us when the Packages object is first instantiated. */ private BuildStoreDB db = null; /** The IPackageMgr object that manages our package types. */ private IPackageMgr pkgMgr = null; /** The IPackageMemberMgr object that manages our package types. */ private IPackageMemberMgr pkgMemberMgr = null; /** The SlotMgr object that manages the slots in our packages. */ private SlotMgr slotMgr = null; /** * Various prepared statements for database access. */ private PreparedStatement addSubPackagePrepStmt = null, insertPackageMemberPrepStmt = null, findSubPackageTypePrepStmt = null, trashOrReviveSubPackagePrepStmt = null, findSubPackagesOfTypePrepStmt = null, isValidOrTrashedPrepStmt = null; /** The event listeners who are registered to learn about sub-package changes */ private List<ISubPackageMgrListener> listeners = new ArrayList<ISubPackageMgrListener>(); /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new Packages object, which represents the file/action packages that * are part of the BuildStore. * * @param buildStore The BuildStore that this Packages object belongs to. */ public SubPackageMgr(BuildStore buildStore) { this.buildStore = buildStore; this.db = buildStore.getBuildStoreDB(); this.pkgMgr = buildStore.getPackageMgr(); this.slotMgr = buildStore.getSlotMgr(); /* initialize prepared database statements */ addSubPackagePrepStmt = db.prepareStatement( "insert into subPackages values (null, ?, 0)"); insertPackageMemberPrepStmt = db.prepareStatement( "insert into packageMembers values (?, ?, ?, ?, -1, -1)"); trashOrReviveSubPackagePrepStmt = db.prepareStatement( "update subPackages set trashed = ? where subPkgId = ? and trashed = ?"); findSubPackageTypePrepStmt = db.prepareStatement( "select pkgTypeId from subPackages where subPkgId = ? and trashed = 0"); findSubPackagesOfTypePrepStmt = db.prepareStatement( "select distinct packageMembers.pkgId from packageMembers, subPackages" + " where packageMembers.memberType = " + IPackageMemberMgr.TYPE_SUB_PACKAGE + " and packageMembers.memberId = subPackages.subPkgId" + " and subPackages.pkgTypeId = ?" + " and subPackages.trashed = 0"); isValidOrTrashedPrepStmt = db.prepareStatement( "select trashed from subPackages where subPkgId = ?"); } /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#newSubPackage(int, int) */ @Override public int newSubPackage(int parentPkgId, int pkgTypeId) { /* validate parent package - must be a valid package (not a folder) */ if (!(pkgMgr.isValid(parentPkgId)) || pkgMgr.isFolder(parentPkgId)) { return ErrorCode.BAD_VALUE; } /* validate package type - must be a valid package (not a folder) */ if (!(pkgMgr.isValid(pkgTypeId)) || pkgMgr.isFolder(pkgTypeId)) { return ErrorCode.NOT_FOUND; } /* we can't create a sub-package of type "Main" or "<import>" */ if ((pkgTypeId == pkgMgr.getMainPackage()) || (pkgTypeId == pkgMgr.getImportPackage())) { return ErrorCode.NOT_FOUND; } /* validate that no cycles are created */ if (isAncestorOf(pkgTypeId, parentPkgId)) { return ErrorCode.LOOP_DETECTED; } /* insert the sub-package information into the database */ int lastRowId; try { addSubPackagePrepStmt.setInt(1, pkgTypeId); db.executePrepUpdate(addSubPackagePrepStmt); lastRowId = db.getLastRowID(); /* * insert the default package membership values (set parent of -1 so we * can change it later on an trigger a notification). */ insertPackageMemberPrepStmt.setInt(1, IPackageMemberMgr.TYPE_SUB_PACKAGE); insertPackageMemberPrepStmt.setInt(2, lastRowId); insertPackageMemberPrepStmt.setInt(3, -1); insertPackageMemberPrepStmt.setInt(4, IPackageMemberMgr.SCOPE_NONE); if (db.executePrepUpdate(insertPackageMemberPrepStmt) != 1) { throw new FatalBuildStoreError("Unable to insert new record into packageMembers table"); } /* * This additional call is required to trigger package membership change notifications. * Without this, our package diagrams won't be refreshed properly. */ pkgMemberMgr.setPackageOfMember(IPackageMemberMgr.TYPE_SUB_PACKAGE, lastRowId, parentPkgId); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } /* return the new package's ID number */ return lastRowId; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#getSubPackageType(int) */ @Override public int getSubPackageType(int subPkgId) { int pkgTypeId = ErrorCode.NOT_FOUND; try { findSubPackageTypePrepStmt.setInt(1, subPkgId); Integer results[] = db.executePrepSelectIntegerColumn(findSubPackageTypePrepStmt); if (results.length == 1){ pkgTypeId = results[0]; } } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } return pkgTypeId; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#isSubPackageValid(int) */ @Override public boolean isSubPackageValid(int subPkgId) { /* * A sub-package is valid if there's a single record for it. We don't actually * care if it's trashed or not - it's still valid. */ try { isValidOrTrashedPrepStmt.setInt(1, subPkgId); Integer rows[] = db.executePrepSelectIntegerColumn(isValidOrTrashedPrepStmt); return (rows.length == 1); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#moveSubPackageToTrash(int) */ @Override public int moveSubPackageToTrash(int subPkgId) { return trashReviveCommon(subPkgId, 1); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#reviveSubPackageFromTrash(int) */ @Override public int reviveSubPackageFromTrash(int subPkgId) { return trashReviveCommon(subPkgId, 0); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#isSubPackageTrashed(int) */ @Override public boolean isSubPackageTrashed(int subPkgId) { /* * A sub-package is trashed if the "trashed" field is set (or if for some reason * there's no record for it). */ try { isValidOrTrashedPrepStmt.setInt(1, subPkgId); Integer rows[] = db.executePrepSelectIntegerColumn(isValidOrTrashedPrepStmt); return (rows.length != 1) || (rows[0] == 1); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#getSlotByName(int, java.lang.String) */ @Override public int getSlotByName(int subPkgId, String slotName) { int pkgId = getSubPackageType(subPkgId); if (pkgId == ErrorCode.NOT_FOUND) { return ErrorCode.NOT_FOUND; } SlotDetails details = pkgMgr.getSlotByName(pkgId, slotName); if (details == null) { return ErrorCode.NOT_FOUND; } return details.slotId; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#setSlotValue(int, int, java.lang.Object) */ @Override public int setSlotValue(int subPkgId, int slotId, Object value) { /* validate subPkgId */ if (!isSubPackageValid(subPkgId)) { return ErrorCode.NOT_FOUND; } /* what is the current slot value? */ Object oldValue = slotMgr.getSlotValue(ISlotTypes.SLOT_OWNER_PACKAGE, subPkgId, slotId); boolean isAlreadySet = isSlotSet(subPkgId, slotId); /* if no change in value, do nothing */ if (isAlreadySet && (((oldValue == null) && (value == null)) || ((oldValue != null) && (oldValue.equals(value))))) { return ErrorCode.OK; } /* delegate all slot assignments to SlotMgr */ int status = slotMgr.setSlotValue(ISlotTypes.SLOT_OWNER_PACKAGE, subPkgId, slotId, value); /* notify listeners about the change */ notifyListeners(subPkgId, ISubPackageMgrListener.CHANGED_SLOT, slotId); return status; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#getSlotValue(int, int) */ @Override public Object getSlotValue(int subPkgId, int slotId) { /* validate subPkgId */ if (!isSubPackageValid(subPkgId)) { return null; } /* delegate all slot assignments to SlotMgr */ return slotMgr.getSlotValue(ISlotTypes.SLOT_OWNER_PACKAGE, subPkgId, slotId); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#isSlotSet(int, int) */ @Override public boolean isSlotSet(int subPkgId, int slotId) { return slotMgr.isSlotSet(ISlotTypes.SLOT_OWNER_PACKAGE, subPkgId, slotId); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#clearSlotValue(int, int) */ @Override public void clearSlotValue(int subPkgId, int slotId) { if (isSlotSet(subPkgId, slotId)) { slotMgr.clearSlotValue(ISlotTypes.SLOT_OWNER_PACKAGE, subPkgId, slotId); notifyListeners(subPkgId, ISubPackageMgrListener.CHANGED_SLOT, slotId); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#addListener(com.buildml.model.ISubPackageMgrListener) */ @Override public void addListener(ISubPackageMgrListener listener) { listeners.add(listener); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.ISubPackageMgr#removeListener(com.buildml.model.ISubPackageMgrListener) */ @Override public void removeListener(ISubPackageMgrListener listener) { listeners.remove(listener); }; /*=====================================================================================* * PACKAGE-PRIVATE METHODS *=====================================================================================*/ /** * A second phase of initialization, needed when managers are fully initialized * in the necessary order. */ public void initPass2() { this.pkgMemberMgr = buildStore.getPackageMemberMgr(); } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * Common code, shared between moveSubPackageToTrash() and reviveSubPackageFromTrash(). * * @param subPkgId ID of the sub-package to trash/revive. * @param toTrashState The state to move it to (1 = trash, 0 = revive). * @return ErrorCode.OK on success, or ErrorCode.NOT_FOUND if subPkgId has no record. */ private int trashReviveCommon(int subPkgId, int toTrashState) { try { trashOrReviveSubPackagePrepStmt.setInt(1, toTrashState); trashOrReviveSubPackagePrepStmt.setInt(2, subPkgId); trashOrReviveSubPackagePrepStmt.setInt(3, 1 - toTrashState); int rowCount = db.executePrepUpdate(trashOrReviveSubPackagePrepStmt); if (rowCount != 1) { return ErrorCode.NOT_FOUND; } } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } /* notify listeners of this event */ notifyListeners(subPkgId, ISubPackageMgrListener.TRASHED_SUB_PACKAGE, 0); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /** * Determine whether pkgA contains pkgB as a sub-package (or sub-sub-package etc). * * @param pkgAId The package that is potentially an ancestor of pkgB. * @param pkgBId The package that is potentially contained within pkgA. * @return True if pkgA contains pkgB, else false. */ private boolean isAncestorOf(int pkgAId, int pkgBId) { /* * If the package IDs are the same, then there's definitely * an ancestor relationship. */ if (pkgAId == pkgBId) { return true; } /* * Work upward from pkgBId until we reach packageMain. */ int pkgMain = pkgMgr.getMainPackage(); /* * Step 1: Find the packages that contain sub-packages of type "pkgBId". */ Integer containingPackages[]; try { findSubPackagesOfTypePrepStmt.setInt(1, pkgBId); containingPackages = db.executePrepSelectIntegerColumn(findSubPackagesOfTypePrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } /* * Step 2: If the containingPackages contains pkgAId, return true. */ for (int i = 0; i < containingPackages.length; i++) { if (containingPackages[i] == pkgAId) { return true; } } /* * Step 3: Call ourselves recursively for each of the containing packages. */ for (int i = 0; i < containingPackages.length; i++) { int thisParent = containingPackages[i]; if ((thisParent != pkgMain) && (isAncestorOf(pkgAId, containingPackages[i]))) { return true; } } return false; } /*-------------------------------------------------------------------------------------*/ /** * Notify any registered listeners about our change in state. * * @param subPkgId The sub-package that has changed. * @param how The way in which the sub-package changed (see {@link ISubPackageMgrListener}). * @param changeId Which thing has changed (CHANGED_SLOT) */ private void notifyListeners(int subPkgId, int how, int changeId) { /* * Make a copy of the listeners list, otherwise a registered listener can't remove * itself from the list within the subPackageChangeNotification() method. */ ISubPackageMgrListener listenerCopy[] = listeners.toArray(new ISubPackageMgrListener[listeners.size()]); for (int i = 0; i < listenerCopy.length; i++) { listenerCopy[i].subPackageChangeNotification(subPkgId, how, changeId); } } /*-------------------------------------------------------------------------------------*/ }