/******************************************************************************* * Copyright (c) 2011 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.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import com.buildml.model.FatalBuildStoreError; import com.buildml.model.IActionMgr; import com.buildml.model.IActionMgrListener; import com.buildml.model.IActionTypeMgr; import com.buildml.model.IBuildStore; import com.buildml.model.IFileMgr; import com.buildml.model.IPackageMemberMgr; import com.buildml.model.IPackageMemberMgr.MemberDesc; import com.buildml.model.IPackageMgr; import com.buildml.model.ISlotTypes; 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 actions. * <p> * There should be exactly one ActionMgr object per BuildStore object. Use the * BuildStore's getActionMgr() method to obtain that one instance. * * @author "Peter Smith <psmith@arapiki.com>" */ public class ActionMgr implements IActionMgr { /*=====================================================================================* * TYPES/FIELDS *=====================================================================================*/ /** * Our database manager object, used to access the database content. This is provided * to us when the ActionMgr object is first instantiated. */ private BuildStoreDB db = null; /** The BuildStore object that owns this ActionMgr object. */ private IBuildStore buildStore = null; /** The FileMgr object associated with this ActionMgr */ private IFileMgr fileMgr = null; /** The PackageMgr object associated with this ActionMgr */ private IPackageMgr pkgMgr = null; /** The PackageMemberMgr object associated with this ActionMgr */ private IPackageMemberMgr pkgMemberMgr = null; /** The SlotMgr object associated with this ActionMgr */ private SlotMgr slotMgr = null; /** Various prepared statement for database access. */ private PreparedStatement insertActionPrepStmt = null, insertPackageMemberPrepStmt = null, findParentPrepStmt = null, updateParentPrepStmt = null, findChildrenPrepStmt = null, insertActionFilesPrepStmt = null, removeActionFilesPrepStmt = null, findFileAccessBySeqnoPrepStmt = null, updateActionFilesPrepStmt = null, findOperationInActionFilesPrepStmt = null, findFilesInActionFilesPrepStmt = null, findFilesByOperationInActionFilesPrepStmt = null, findActionsByFileInActionFilesPrepStmt = null, findActionsByFileAndOperationInActionFilesPrepStmt = null, trashActionPrepStmt = null, actionIsTrashPrepStmt = null, findActionTypePrepStmt = null; /** The event listeners who are registered to learn about action changes */ private List<IActionMgrListener> listeners = new ArrayList<IActionMgrListener>(); /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new ActionMgr object. This object encapsulates information for all the build * actions in the system. * * @param buildStore The BuildStore object that "owns" this ActionMgr manager */ public ActionMgr(BuildStore buildStore) { this.buildStore = buildStore; this.db = buildStore.getBuildStoreDB(); this.fileMgr = buildStore.getFileMgr(); this.slotMgr = buildStore.getSlotMgr(); /* create prepared database statements */ insertActionPrepStmt = db.prepareStatement("insert into buildActions values (null, 0, 0, ?)"); insertPackageMemberPrepStmt = db.prepareStatement("insert into packageMembers values (?, ?, ?, ?, -1, -1)"); findParentPrepStmt = db.prepareStatement("select parentActionId from buildActions where actionId = ?"); updateParentPrepStmt = db.prepareStatement("update buildActions set parentActionId = ? where actionId = ?"); findChildrenPrepStmt = db.prepareStatement("select actionId from buildActions where parentActionId = ?" + " and (parentActionId != actionId) and (trashed = 0) order by actionId"); insertActionFilesPrepStmt = db.prepareStatement("insert into actionFiles values (?, ?, ?, ?)"); removeActionFilesPrepStmt = db.prepareStatement("delete from actionFiles where actionId = ? and fileId = ?"); findFileAccessBySeqnoPrepStmt = db.prepareStatement("select seqno, actionId, fileId, operation from actionFiles where seqno = ?"); updateActionFilesPrepStmt = db.prepareStatement("update actionFiles set operation = ? where actionId = ? and fileId = ?"); findOperationInActionFilesPrepStmt = db.prepareStatement("select operation from actionFiles where actionId = ? and fileId = ?"); findFilesInActionFilesPrepStmt = db.prepareStatement("select fileId from actionFiles where actionId = ?"); findFilesByOperationInActionFilesPrepStmt = db.prepareStatement("select fileId from actionFiles where actionId = ? and operation = ?"); findActionsByFileInActionFilesPrepStmt = db.prepareStatement("select actionId from actionFiles where fileId = ?"); findActionsByFileAndOperationInActionFilesPrepStmt = db.prepareStatement("select actionId from actionFiles where fileId = ? and operation = ?"); trashActionPrepStmt = db.prepareStatement("update buildActions set trashed = ? where actionId = ?"); actionIsTrashPrepStmt = db.prepareStatement("select trashed from buildActions where actionId = ?"); findActionTypePrepStmt = db.prepareStatement("select actionType from buildActions where actionId = ?"); } /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#addAction(int, int) */ @Override public int addAction(int actionTypeId) { this.pkgMgr = buildStore.getPackageMgr(); IActionTypeMgr actionTypeMgr = buildStore.getActionTypeMgr(); if (!actionTypeMgr.isValid(actionTypeId) || actionTypeMgr.isFolder(actionTypeId)) { return ErrorCode.NOT_FOUND; } int lastRowId; try { insertActionPrepStmt.setInt(1, actionTypeId); db.executePrepUpdate(insertActionPrepStmt); lastRowId = db.getLastRowID(); if (lastRowId >= MAX_ACTIONS) { throw new FatalBuildStoreError("Exceeded maximum action number: " + MAX_ACTIONS); } /* insert the default package membership values */ insertPackageMemberPrepStmt.setInt(1, IPackageMemberMgr.TYPE_ACTION); insertPackageMemberPrepStmt.setInt(2, lastRowId); insertPackageMemberPrepStmt.setInt(3, pkgMgr.getImportPackage()); insertPackageMemberPrepStmt.setInt(4, IPackageMemberMgr.SCOPE_NONE); if (db.executePrepUpdate(insertPackageMemberPrepStmt) != 1) { throw new FatalBuildStoreError("Unable to insert new record into packageMembers table"); } } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } return lastRowId; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#addAction(int, int, java.lang.String) */ @Override public int addShellCommandAction(int parentActionId, int actionDirId, String command) { /* create a new action of type "Shell Command" */ int newActionId = addAction(ActionTypeMgr.BUILTIN_SHELL_COMMAND_ID); if (newActionId < 0) { return newActionId; } /* set the action's parent */ int rc = setParent(newActionId, parentActionId); if (rc != ErrorCode.OK) { return rc; } /* set the action's command string */ rc = setSlotValue(newActionId, IActionMgr.COMMAND_SLOT_ID, command); if (rc != ErrorCode.OK) { return rc; } /* set the action's working directory */ rc = setSlotValue(newActionId, IActionMgr.DIRECTORY_SLOT_ID, actionDirId); if (rc != ErrorCode.OK) { return rc; } return newActionId; } /*-------------------------------------------------------------------------------------*/ /** * A two-dimensional mapping table for tracking the state of each file access. If a file * is accessed multiple times by a single action, the state of that access can also change. * Given an "existing" file-access state, and a "new" file-access state, this matrix tells * us the state to transition to. For example, if "existing" is OP_READ and "new" is * OP_WRITE, then the combined state is OP_MODIFIED. */ private OperationType operationTypeMapping[][] = { { /* For Existing == OP_UNSPECIFIED */ OperationType.OP_UNSPECIFIED, /* New == OP_UNSPECIFIED */ OperationType.OP_UNSPECIFIED, /* New == OP_READ */ OperationType.OP_UNSPECIFIED, /* New == OP_WRITE */ OperationType.OP_UNSPECIFIED, /* New == OP_MODIFIED */ OperationType.OP_UNSPECIFIED /* New == OP_DELETE */ }, { /* For Existing == OP_READ */ OperationType.OP_UNSPECIFIED, /* New == OP_UNSPECIFIED */ OperationType.OP_READ, /* New == OP_READ */ OperationType.OP_MODIFIED, /* New == OP_WRITE */ OperationType.OP_MODIFIED, /* New == OP_MODIFIED */ OperationType.OP_DELETE /* New == OP_DELETE */ }, { /* For Existing == OP_WRITE */ OperationType.OP_UNSPECIFIED, /* New == OP_UNSPECIFIED */ OperationType.OP_WRITE, /* New == OP_READ */ OperationType.OP_WRITE, /* New == OP_WRITE */ OperationType.OP_WRITE, /* New == OP_MODIFIED */ OperationType.OP_DELETE /* New == OP_DELETE */ }, { /* For Existing == OP_MODIFIED */ OperationType.OP_UNSPECIFIED, /* New == OP_UNSPECIFIED */ OperationType.OP_MODIFIED, /* New == OP_READ */ OperationType.OP_MODIFIED, /* New == OP_WRITE */ OperationType.OP_MODIFIED, /* New == OP_MODIFIED */ OperationType.OP_DELETE /* New == OP_DELETE */ }, { /* For Existing == OP_DELETED */ OperationType.OP_UNSPECIFIED, /* New == OP_UNSPECIFIED */ OperationType.OP_READ, /* New == OP_READ */ OperationType.OP_WRITE, /* New == OP_WRITE */ OperationType.OP_MODIFIED, /* New == OP_MODIFIED */ OperationType.OP_DELETE /* New == OP_DELETE */ } }; /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#addFileAccess(int, int, com.buildml.model.IActionMgr.OperationType) */ @Override public void addFileAccess(int actionId, int fileId, OperationType newOperation) { addFileAccessCommon(-1, actionId, fileId, newOperation); } /*-------------------------------------------------------------------------------------*/ /* * (non-Javadoc) * @see com.buildml.model.IActionMgr#addSequencedFileAccess(int, int, int, com.buildml.model.IActionMgr.OperationType) */ public int addSequencedFileAccess(int seqno, int actionId, int fileId, OperationType newOperation) { /* check if there's already a file access with the required sequence number */ if (seqno != -1) { Integer intResults[] = null; try { findFileAccessBySeqnoPrepStmt.setInt(1, seqno); intResults = db.executePrepSelectIntegerColumn(findFileAccessBySeqnoPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } if (intResults.length != 0) { return ErrorCode.ONLY_ONE_ALLOWED; } } /* proceed to add the access, possibly merging it with existing actions */ addFileAccessCommon(seqno, actionId, fileId, newOperation); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getFilesAccessed(int, com.buildml.model.IActionMgr.OperationType) */ @Override public Integer [] getFilesAccessed(int actionId, OperationType operation) { List<Integer> results; try { ResultSet rs; /* if we want all operation (OP_UNSPECIFIED), don't query the operation field */ if (operation == OperationType.OP_UNSPECIFIED) { findFilesInActionFilesPrepStmt.setInt(1, actionId); rs = db.executePrepSelectResultSet(findFilesInActionFilesPrepStmt); } /* else, we need to limit the results, based on the operation */ else { findFilesByOperationInActionFilesPrepStmt.setInt(1, actionId); findFilesByOperationInActionFilesPrepStmt.setInt(2, operation.ordinal()); rs = db.executePrepSelectResultSet(findFilesByOperationInActionFilesPrepStmt); } /* read the results into an array */ results = new ArrayList<Integer>(); while (rs.next()) { results.add(rs.getInt(1)); } rs.close(); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } return results.toArray(new Integer[0]); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getSequencedFileAccesses(int[]) */ @Override public FileAccess[] getSequencedFileAccesses(Integer[] actionIds) { List<FileAccess> results = new ArrayList<FileAccess>(); try { ResultSet rs; /* Form a comma-separated list of action IDs */ StringBuilder actSb = new StringBuilder(); int length = actionIds.length; for (int i = 0; i < length; i++) { actSb.append(actionIds[i]); if (i != length - 1) { actSb.append(", "); } } String actionsString = actSb.toString(); /* query for all actionFile entries for any of the specified actions */ String stmt = "select seqno, actionId, fileId, operation from actionFiles " + "where actionId in (" + actionsString + ") order by seqno;"; rs = db.executeSelectResultSet(stmt); /* read the results into an array */ results = new ArrayList<FileAccess>(); while (rs.next()) { FileAccess access = new FileAccess(); access.seqno = rs.getInt(1); access.actionId = rs.getInt(2); access.pathId = rs.getInt(3); access.opType = intToOperationType(rs.getInt(4)); results.add(access); } rs.close(); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } /* convert from List to FileAccess[] */ return results.toArray(new FileAccess[results.size()]); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getActionsThatAccess(int, com.buildml.model.IActionMgr.OperationType) */ @Override public Integer [] getActionsThatAccess(int fileId, OperationType operation) { List<Integer> results; try { ResultSet rs; /* if we want all operation (OP_UNSPECIFIED), don't query the operation field */ if (operation == OperationType.OP_UNSPECIFIED) { findActionsByFileInActionFilesPrepStmt.setInt(1, fileId); rs = db.executePrepSelectResultSet(findActionsByFileInActionFilesPrepStmt); } /* else, we need to limit the results, based on the operation */ else { findActionsByFileAndOperationInActionFilesPrepStmt.setInt(1, fileId); findActionsByFileAndOperationInActionFilesPrepStmt.setInt(2, operation.ordinal()); rs = db.executePrepSelectResultSet(findActionsByFileAndOperationInActionFilesPrepStmt); } /* read the results into an array */ results = new ArrayList<Integer>(); while (rs.next()) { results.add(rs.getInt(1)); } rs.close(); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } return results.toArray(new Integer[0]); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#removeAccessesToPath(int) */ @Override public void removeFileAccess(int actionId, int pathId) { try { removeActionFilesPrepStmt.setInt(1, actionId); removeActionFilesPrepStmt.setInt(2, pathId); db.executePrepUpdate(removeActionFilesPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getActionType(int) */ @Override public int getActionType(int actionId) { Integer intResults[] = null; try { findActionTypePrepStmt.setInt(1, actionId); intResults = db.executePrepSelectIntegerColumn(findActionTypePrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } if (intResults.length == 0) { return ErrorCode.NOT_FOUND; } return intResults[0]; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getParent(int) */ @Override public int getParent(int actionId) { /* query the database, based on the action Id */ Integer [] intResults = null; try { findParentPrepStmt.setInt(1, actionId); intResults = db.executePrepSelectIntegerColumn(findParentPrepStmt); } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } /* if there were no results, it's because actionId is invalid. Return an error */ if (intResults.length == 0) { return ErrorCode.BAD_VALUE; } /* if there was one result, return it */ else if (intResults.length == 1) { /* the single result is the parent, unless this action's parent is itself! */ int parentActionId = intResults[0]; if (parentActionId == actionId) { /* the current action is at the root */ return ErrorCode.NOT_FOUND; } return parentActionId; } /* else, multiple results is a bad thing */ else { throw new FatalBuildStoreError("Multiple results find in buildActions table for actionId = " + actionId); } } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#setParent(int, int) */ @Override public int setParent(int actionId, int newParentId) { /* we can't be our own parent */ if (actionId == newParentId) { return ErrorCode.BAD_VALUE; } /* we can't change the parent of the root */ if (actionId == getRootAction("root")) { return ErrorCode.BAD_VALUE; } /* * Check that we are not an ancestor of our new parent (this would cause * a loop in the tree - not allowed). Loop upwards through the tree, * until we reach the root (parent is NOT_FOUND). */ int ancestorId = newParentId; while (ancestorId != ErrorCode.NOT_FOUND) { ancestorId = getParent(ancestorId); /* is newParentId a bad action ID? */ if (ancestorId == ErrorCode.BAD_VALUE) { return ErrorCode.BAD_VALUE; } /* else, is actionId in the ancestor chain of newParentId? */ if (actionId == ancestorId) { return ErrorCode.BAD_VALUE; } } /* attempt to update the existing record for actionId */ try { updateParentPrepStmt.setInt(1, newParentId); updateParentPrepStmt.setInt(2, actionId); if (db.executePrepUpdate(updateParentPrepStmt) != 1) { /* there was no record for actionId */ return ErrorCode.BAD_VALUE; } } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getChildren(int) */ @Override public Integer [] getChildren(int actionId) { Integer [] intResults = null; try { findChildrenPrepStmt.setInt(1, actionId); intResults = db.executePrepSelectIntegerColumn(findChildrenPrepStmt); } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } return intResults; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getRootAction(java.lang.String) */ @Override public int getRootAction(String rootName) { // TODO: return something other than 0. Currently the default action is created // implicitly, rather than explicitly return 0; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#moveActionToTrash(int) */ @Override public int moveActionToTrash(int actionId) { /* check that the action is not the root action */ if (getParent(actionId) == ErrorCode.NOT_FOUND) { return ErrorCode.CANT_REMOVE; } /* check that the action has no children */ Integer children[] = getChildren(actionId); if (children.length != 0) { return ErrorCode.CANT_REMOVE; } /* check that the action is not referenced by a file */ Integer filesAccess[] = getFilesAccessed(actionId, OperationType.OP_UNSPECIFIED); if (filesAccess.length != 0) { return ErrorCode.CANT_REMOVE; } /* mark action as trashed */ try { trashActionPrepStmt.setInt(1, 1); trashActionPrepStmt.setInt(2, actionId); db.executePrepUpdate(trashActionPrepStmt); } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } /* notify listeners of the change */ notifyListeners(actionId, IActionMgrListener.TRASHED_ACTION, 0); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#reviveActionFromTrash(int) */ @Override public int reviveActionFromTrash(int actionId) { /* first, check that the parent action is not trashed */ int parentId = getParent(actionId); if ((parentId == ErrorCode.NOT_FOUND) || (parentId == ErrorCode.BAD_VALUE)) { return ErrorCode.CANT_REVIVE; } if (isActionTrashed(parentId)) { return ErrorCode.CANT_REVIVE; } /* mark action as not trashed */ try { trashActionPrepStmt.setInt(1, 0); trashActionPrepStmt.setInt(2, actionId); db.executePrepUpdate(trashActionPrepStmt); } catch (SQLException e) { new FatalBuildStoreError("Error in SQL: " + e); } /* notify listeners of the change */ notifyListeners(actionId, IActionMgrListener.TRASHED_ACTION, 0); return ErrorCode.OK; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#isActionValid(int) */ @Override public boolean isActionValid(int actionId) { /* all valid actions have parents */ return getParent(actionId) != ErrorCode.BAD_VALUE; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#isActionTrashed(int) */ @Override public boolean isActionTrashed(int actionId) { Integer results[] = null; try { actionIsTrashPrepStmt.setInt(1, actionId); results = db.executePrepSelectIntegerColumn(actionIsTrashPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Error in SQL: " + e); } /* action isn't even known - let's assume it's trashed */ if (results.length != 1) { return true; } /* if "trashed" field is 1, then the action is trashed */ return results[0] == 1; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getSlotByName(int, java.lang.String) */ @Override public int getSlotByName(int actionId, String slotName) { IActionTypeMgr actionTypeMgr = buildStore.getActionTypeMgr(); int actionTypeId = getActionType(actionId); if (actionTypeId == ErrorCode.NOT_FOUND) { return ErrorCode.NOT_FOUND; } SlotDetails details = actionTypeMgr.getSlotByName(actionTypeId, slotName); if (details == null) { return ErrorCode.NOT_FOUND; } return details.slotId; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#setSlotValue(int, int, java.lang.Object) */ @Override public int setSlotValue(int actionId, int slotId, Object value) { /* validate actionId */ if (!isActionValid(actionId)) { return ErrorCode.NOT_FOUND; } /* what is the current slot value? */ Object oldValue = slotMgr.getSlotValue(ISlotTypes.SLOT_OWNER_ACTION, actionId, slotId); boolean isAlreadySet = isSlotSet(actionId, slotId); /* if no change in value, do nothing */ if (isAlreadySet && (((oldValue == null) && (value == null)) || ((oldValue != null) && (oldValue.equals(value))))) { return ErrorCode.OK; } /* * If the slot is an input or output, we first need to check for cycles in the graph. */ SlotDetails details = slotMgr.getSlotByID(slotId); if (details == null) { return ErrorCode.NOT_FOUND; } if (((details.slotPos == ISlotTypes.SLOT_POS_INPUT) || (details.slotPos == ISlotTypes.SLOT_POS_OUTPUT)) && (value instanceof Integer)) { /* yes, we're inserting a file group into an action. Check for cycles */ int direction = (details.slotPos == ISlotTypes.SLOT_POS_OUTPUT) ? IPackageMemberMgr.NEIGHBOUR_LEFT : IPackageMemberMgr.NEIGHBOUR_RIGHT; if (checkForCycles(IPackageMemberMgr.TYPE_ACTION, actionId, (Integer)value, direction)) { return ErrorCode.LOOP_DETECTED; } } /* delegate all slot assignments to SlotMgr */ int status = slotMgr.setSlotValue(ISlotTypes.SLOT_OWNER_ACTION, actionId, slotId, value); /* notify listeners about the change */ notifyListeners(actionId, IActionMgrListener.CHANGED_SLOT, slotId); return status; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getSlotValue(int, int) */ @Override public Object getSlotValue(int actionId, int slotId) { /* validate actionId */ if (!isActionValid(actionId)) { return null; } /* delegate all slot assignments to SlotMgr */ return slotMgr.getSlotValue(ISlotTypes.SLOT_OWNER_ACTION, actionId, slotId); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#isSlotSet(int, int) */ @Override public boolean isSlotSet(int actionId, int slotId) { return slotMgr.isSlotSet(ISlotTypes.SLOT_OWNER_ACTION, actionId, slotId); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#clearSlotValue(int, int) */ @Override public void clearSlotValue(int actionId, int slotId) { slotMgr.clearSlotValue(ISlotTypes.SLOT_OWNER_ACTION, actionId, slotId); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getActionsWhereSlotIsLike(int, java.lang.String) */ @Override public Integer[] getActionsWhereSlotIsLike(int slotId, String match) { return slotMgr.getOwnersWhereSlotIsLike(ISlotTypes.SLOT_OWNER_ACTION, slotId, match); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getActionsWhereSlotEquals(int, java.lang.String) */ @Override public Integer[] getActionsWhereSlotEquals(int slotId, Object match) { return slotMgr.getOwnersWhereSlotEquals(ISlotTypes.SLOT_OWNER_ACTION, slotId, match); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#getBuildStore() */ @Override public IBuildStore getBuildStore() { return buildStore; } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#addListener(com.buildml.model.IActionMgrListener) */ @Override public void addListener(IActionMgrListener listener) { listeners.add(listener); } /*-------------------------------------------------------------------------------------*/ /* (non-Javadoc) * @see com.buildml.model.IActionMgr#removeListener(com.buildml.model.IActionMgrListener) */ @Override public void removeListener(IActionMgrListener listener) { listeners.remove(listener); }; /*=====================================================================================* * PACKAGE METHODS *=====================================================================================*/ /** * Extra initialization that can only happen all other managers are initialized. */ /* package */ void initPass2() { /* empty for now */ } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * Helper function for translating from an ordinal integer to an OperationType. This is the * opposite of OperationType.ordinal(). * * @param opTypeNum The ordinal value of a OperationType value. * @return The corresponding OperationType value. * @throws FatalBuildStoreError if the ordinal value is out of range. */ private OperationType intToOperationType(int opTypeNum) throws FatalBuildStoreError { switch (opTypeNum) { case 0: return OperationType.OP_UNSPECIFIED; case 1: return OperationType.OP_READ; case 2: return OperationType.OP_WRITE; case 3: return OperationType.OP_MODIFIED; case 4: return OperationType.OP_DELETE; default: throw new FatalBuildStoreError("Invalid value found in operation field: " + opTypeNum); } } /*-------------------------------------------------------------------------------------*/ /** * A helper method for addFileAccess() and addSequencedFileAccess(). * * @param seqno The sequence number to use for the new file-access, or -1 if we should use * the next available number. * @param actionId The action that performs the access. * @param fileId The file that is accessed. * @param newOperation The operation type of the access. */ private void addFileAccessCommon(int seqno, int actionId, int fileId, OperationType newOperation) { /* * We don't want to add the same record twice, but we might want to merge the two * operations together. That is, if a action reads a file, then writes a file, we want * to mark it as OP_MODIFIED. */ Integer intResults[] = null; try { findOperationInActionFilesPrepStmt.setInt(1, actionId); findOperationInActionFilesPrepStmt.setInt(2, fileId); intResults = db.executePrepSelectIntegerColumn(findOperationInActionFilesPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } /* * If there was no existing record, we'll insert a fresh record. */ if (intResults.length == 0) { try { if (seqno == -1) { insertActionFilesPrepStmt.setNull(1, java.sql.Types.INTEGER); } else { insertActionFilesPrepStmt.setInt(1, seqno); } insertActionFilesPrepStmt.setInt(2, actionId); insertActionFilesPrepStmt.setInt(3, fileId); insertActionFilesPrepStmt.setInt(4, newOperation.ordinal()); db.executePrepUpdate(insertActionFilesPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } } /* * Else, if there's one record, see if the operation needs to be merged. The DFA * for transitioning to a new state is as follows: * * New: | Read Write Modify Delete * ----------------------------------------- * Read | Read Modify Modify Delete * Existing: Write | Write Write Write Temporary * Modify | Modify Modify Modify Delete * Delete | Read Write Modify Delete * * Remember: * - Read = the process has *only* ever read this file. * - Write = the process created this file (it didn't exist before). * - Modify = the process read and then wrote to this file. * - Delete = the process ended up by deleting this file. */ else if (intResults.length == 1) { OperationType existingOp = intToOperationType(intResults[0]); OperationType combinedOp = operationTypeMapping[existingOp.ordinal()][newOperation.ordinal()]; /* * Handle a special case of temporary files. That is, if the existingOp is WRITE, * and the combinedOp is DELETE, then this file was both created and deleted * by this action. */ if ((existingOp == OperationType.OP_WRITE) && (combinedOp == OperationType.OP_DELETE)) { /* remove all file accesses that we previously added */ removeFileAccess(actionId, fileId); /* * Attempt to remove the file from the FileMgr. This will fail if the * same path is already used by some other action, but that's acceptable. We * only want to remove paths that were used exclusively by this action. */ fileMgr.movePathToTrash(fileId); } /* * else, the normal case is to replace the old state with the new state. */ else { try { updateActionFilesPrepStmt.setInt(1, combinedOp.ordinal()); updateActionFilesPrepStmt.setInt(2, actionId); updateActionFilesPrepStmt.setInt(3, fileId); db.executePrepUpdate(updateActionFilesPrepStmt); } catch (SQLException e) { throw new FatalBuildStoreError("Unable to execute SQL statement", e); } } } /* else, there's an error - can't have multiple entries */ else { throw new FatalBuildStoreError("Multiple results find in actionFiles table for actionId = " + actionId + " and fileId = " + fileId); } } /*-------------------------------------------------------------------------------------*/ /** * Notify any registered listeners about our change in state. * @param actionId The action that has changed. * @param how The way in which the action changed (see {@link IActionMgrListener}). * @param changeId Which thing has changed (CHANGED_SLOT) */ private void notifyListeners(int actionId, int how, int changeId) { /* * Make a copy of the listeners list, otherwise a registered listener can't remove * itself from the list within the actionChangeNotification() method. */ IActionMgrListener listenerCopy[] = listeners.toArray(new IActionMgrListener[listeners.size()]); for (int i = 0; i < listenerCopy.length; i++) { listenerCopy[i].actionChangeNotification(actionId, how, changeId); } } /*-------------------------------------------------------------------------------------*/ /** * If we're modifying an action's input or output slot, we could potentially be creating * a cycle in the dependency graph. Before allowing this addition, check whether it * would create a cycle. This is a recursive algorithm that searches to the left/right in search * of the file group to be added. For example, if we're insert a file group into an action's * output slot, search left (through the inputs) in search of that file group. * * @param memberType What are we currently looking at? (IPackageMemberMgr.TYPE_ACTION, etc). * @param memberId The ID of the member we're currently looking at (initially the action * containing the slot to be changed). * @param fileGroupId The ID of the file group that's being inserted into the slot. * @param direction The direction to search (IPackageMemberMgr.NEIGHBOUR_LEFT or * IPackageMemberMgr.NEIGHBOUR_RIGHT). * @return True if a cycle would be created, else false. */ private boolean checkForCycles(int memberType, int memberId, int fileGroupId, int direction) { pkgMemberMgr = buildStore.getPackageMemberMgr(); /* get neighbours of this member */ MemberDesc[] neighbours = pkgMemberMgr.getNeighboursOf(memberType, memberId, direction, false); for (int i = 0; i < neighbours.length; i++) { MemberDesc neighbour = neighbours[i]; /* if we've hit the file group we're searching for - end the search */ if ((neighbour.memberType == IPackageMemberMgr.TYPE_FILE_GROUP) && (neighbour.memberId == fileGroupId)) { return true; } /* not found, recursively search our neighbours */ if (checkForCycles(neighbour.memberType, neighbour.memberId, fileGroupId, direction)) { return true; } /* now loop to the next neighbour */ } return false; } /*-------------------------------------------------------------------------------------*/ }