/*******************************************************************************
* Copyright (c) 2013 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.refactor.imports;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.buildml.model.IActionMgr;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileGroupMgr;
import com.buildml.model.IFileMgr;
import com.buildml.model.IActionMgr.OperationType;
import com.buildml.model.IFileMgr.PathType;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.IPackageMemberMgr.MemberDesc;
import com.buildml.model.IPackageMemberMgr.PackageDesc;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageRootMgr;
import com.buildml.model.undo.ActionUndoOp;
import com.buildml.model.undo.FileGroupUndoOp;
import com.buildml.model.undo.FileUndoOp;
import com.buildml.model.undo.MultiUndoOp;
import com.buildml.refactor.CanNotRefactorException;
import com.buildml.refactor.CanNotRefactorException.Cause;
import com.buildml.utils.errors.ErrorCode;
import com.buildml.utils.errors.FatalError;
/**
* An object of this type is used for handling a single "move to package" operation,
* which takes one or more existing package members (files, file group, actions, etc)
* and moves them to a new package.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class MovePackageRefactorer {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** ID of the package we're moving members into */
private int destPkgId;
/** The MultiUndoOp that we'll append all our operations to */
private MultiUndoOp multiOp;
/** The build store that members belong to */
private IBuildStore buildStore;
/** Package manager for this buildStore */
private IPackageMgr pkgMgr;
/** Action manager for this buildStore */
private IActionMgr actionMgr;
/** File manager for this buildStore */
private IFileMgr fileMgr;
/** File Group manager for this buildStore */
private IFileGroupMgr fileGroupMgr;
/** Package member manager for this buildStore */
private IPackageMemberMgr pkgMemberMgr;
/** Package Root manager for this buildStore */
private IPackageRootMgr pkgRootMgr;
/** The destination package's path root ID */
private int pkgRootId;
/** The cache of action->fileGroup mapping, used to avoid re-importing actions multiple times */
private Map<Integer, Integer> actionCache;
/**
* The cache of fileGroupId->members. Given that file groups aren't actually created in
* the database until the refactoring is complete, we can't query members from the DB.
*/
private Map<Integer, List<Integer>> fileGroupCache;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new {@link MovePackageRefactorer} object.
*
* @param destPkgId ID of the package to move members into.
* @param multiOp MultiUndoOp to add change operations to.
* @param buildStore The IBuildStore that stores the packages/members.
*/
public MovePackageRefactorer(int destPkgId, MultiUndoOp multiOp, IBuildStore buildStore) {
this.destPkgId = destPkgId;
this.multiOp = multiOp;
this.buildStore = buildStore;
pkgMgr = buildStore.getPackageMgr();
actionMgr = buildStore.getActionMgr();
fileMgr = buildStore.getFileMgr();
fileGroupMgr = buildStore.getFileGroupMgr();
pkgMemberMgr = buildStore.getPackageMemberMgr();
pkgRootMgr = buildStore.getPackageRootMgr();
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* This is the main entry point for moving package members from one package to another.
*
* @param members The list of members to move.
* @throws CanNotRefactorException Something went wrong with the move operation.
*/
public void moveMembersToPackage(List<MemberDesc> members) throws CanNotRefactorException {
/* Validate that the destination package ID is valid */
if (!pkgMgr.isValid(destPkgId)) {
throw new CanNotRefactorException(Cause.INVALID_PACKAGE, destPkgId);
}
/*
* We'll need to know the package's source root ID. Files can't be moved into
* a package if they're not within this root.
*/
pkgRootId = pkgRootMgr.getPackageRoot(destPkgId, IPackageRootMgr.SOURCE_ROOT);
if (pkgRootId == ErrorCode.NOT_FOUND) {
throw new FatalError("Unrecognized package ID");
}
/* validate all members passed as input - this will throw an exception if there's an error */
validateMembersList(members);
/* initialize the action->fileGroup mapping cache and the fileGroupId-> members cache */
actionCache = new HashMap<Integer, Integer>();
fileGroupCache = new HashMap<Integer, List<Integer>>();
/*
* Now, traverse the list of input members and separate them into groups:
* 1) Loose files that aren't generated by an action (these will be placed in their
* own file group).
* 2) Files that are generated by a single action (we care about that action, rather
* than the file, and simply treat it as if the caller had asked to move the action).
* 3) Files that are generated by multiple actions (i.e. modified). This is an error.
* 4) Actions that the caller has explicitly asked to move to the destination package.
*/
List<Integer> looseMembers = new ArrayList<Integer>();
List<Integer> badFiles = new ArrayList<Integer>();
List<Integer> actions = new ArrayList<Integer>();
/* for each input member that is a file or an action... */
for (MemberDesc member : members) {
int id = member.memberId;
int type = member.memberType;
/* for files, determine if it's generated or not */
if (type == IPackageMemberMgr.TYPE_FILE) {
/* Check for actions that "modify" this file (which is invalid) */
Integer[] modifyingActions = actionMgr.getActionsThatAccess(id, OperationType.OP_MODIFIED);
if (modifyingActions.length > 0) {
badFiles.add(id);
}
/*
* Check for actions that write this file (only one action can write, else it's
* invalid). Zero writers = loose file, One writer = record the action, Multiple writers =
* an error.
*/
else {
Integer[] generatorActions = actionMgr.getActionsThatAccess(id, OperationType.OP_WRITE);
int numActions = generatorActions.length;
if (numActions == 0) {
looseMembers.add(id);
} else if (numActions == 1) {
int generatorAction = generatorActions[0];
if (!actions.contains(generatorAction)) {
actions.add(generatorAction);
}
} else {
badFiles.add(id);
}
}
}
/* else record all actions for later processing */
else if (type == IPackageMemberMgr.TYPE_ACTION) {
if (!actions.contains(id)) {
actions.add(id);
}
}
}
/*
* Were there any files that were generated by multiple actions (or modified by an action)?
* If so, throw an error.
*/
if (badFiles.size() != 0) {
throw new CanNotRefactorException(Cause.FILE_IS_MODIFIED, badFiles.toArray(new Integer[0]));
}
/*
* If there are any loose files, create a source file group and populate it with those files.
* This becomes a standalone file group that appears on the diagram, without any generator
* actions.
*/
if (looseMembers.size() > 0) {
createSourceFileGroup(looseMembers);
}
/*
* For each action, move the action and any predecessor files/filegroups/actions to the package.
* It's these calls that manage the recursion through the chain of imported files/actions.
*/
for (int actionId : actions) {
moveActionToPackage(actionId);
}
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Validate the list of members that is input into moveMembersToPackage(). There are
* many possible errors, including invalid member types, or undefined action, file,
* or fileGroup ID numbers. Errors are report via exceptions.
*
* @param members The list of MemberDesc to be validated.
* @throws CanNotRefactorException The reason why the members list is invalid.
*/
private void validateMembersList(List<MemberDesc> members) throws CanNotRefactorException {
IFileMgr fileMgr = buildStore.getFileMgr();
IFileGroupMgr fileGroupMgr = buildStore.getFileGroupMgr();
IActionMgr actionMgr = buildStore.getActionMgr();
/* can't be null! */
if (members == null) {
throw new CanNotRefactorException(Cause.INVALID_MEMBER, -1);
}
/* keep a lists of invalid "things" - we can provide the whole list in the exception */
List<Integer> invalidFiles = new ArrayList<Integer>();
List<Integer> invalidFileGroups = new ArrayList<Integer>();
List<Integer> invalidActions = new ArrayList<Integer>();
/*
* Scan the list of members, validating their type, and whether each ID is valid.
* Invalid entries are logged, and will be reported as exceptions once we've seen
* all the members.
*/
for (MemberDesc member: members) {
int id = member.memberId;
switch (member.memberType) {
case IPackageMemberMgr.TYPE_FILE:
PathType pathType = fileMgr.getPathType(id);
if (pathType == PathType.TYPE_INVALID) {
invalidFiles.add(id);
}
break;
case IPackageMemberMgr.TYPE_FILE_GROUP:
if (fileGroupMgr.getGroupType(id) == ErrorCode.NOT_FOUND) {
invalidFileGroups.add(id);
}
break;
case IPackageMemberMgr.TYPE_ACTION:
if (!actionMgr.isActionValid(id)) {
invalidActions.add(id);
}
break;
default:
throw new CanNotRefactorException(Cause.INVALID_MEMBER, member.memberType);
}
}
/* Thrown exceptions if we found anything invalid */
if (!invalidFiles.isEmpty()) {
throw new CanNotRefactorException(Cause.INVALID_PATH, invalidFiles.toArray(new Integer[0]));
}
if (!invalidActions.isEmpty()) {
throw new CanNotRefactorException(Cause.INVALID_ACTION, invalidActions.toArray(new Integer[0]));
}
if (!invalidFileGroups.isEmpty()) {
throw new CanNotRefactorException(Cause.INVALID_FILE_GROUP, invalidFileGroups.toArray(new Integer[0]));
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Given an actionId, move the action, and possibly all its predecessors to the new package.
* If necessary, an output file group is created and also moved to the new package. So
* too are all the files in that groups.
*
* @param actionId ID of the action to be moved.
* @return ID of the output file group, or ErrorCode.NOT_FOUND if there's no output group.
* @throws CanNotRefactorException If something goes wrong.
*/
private int moveActionToPackage(int actionId) throws CanNotRefactorException {
/* first, check to see if this action is already imported this package - if so, we're done */
Object existingMap = actionCache.get(Integer.valueOf(actionId));
if (existingMap instanceof Integer) {
return (Integer)existingMap;
}
/* validate that the action is atomic */
Integer children[] = actionMgr.getChildren(actionId);
if (children.length != 0) {
throw new CanNotRefactorException(Cause.ACTION_NOT_ATOMIC, actionId);
}
/*
* The action is not yet imported, and it is atomic. Figure out its current package
* and prepare to move it to the new package.
*/
int fileGroupId = ErrorCode.NOT_FOUND;
PackageDesc desc = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_ACTION, actionId);
if (desc == null) {
throw new FatalError("Can't retrieve action's current package");
}
/* Create UndoOp for changing the action's package */
ActionUndoOp actionOp = new ActionUndoOp(buildStore, actionId);
actionOp.recordPackageChange(desc.pkgId, destPkgId);
multiOp.add(actionOp);
/* Create a new file group containing all the files that this action generates */
Integer writtenFiles[] = actionMgr.getFilesAccessed(actionId, OperationType.OP_WRITE);
if (writtenFiles.length > 0) {
fileGroupId = createSourceFileGroup(Arrays.asList(writtenFiles));
/* Connect the "output" slot from the action to this new file group */
ActionUndoOp slotOp = new ActionUndoOp(buildStore, actionId);
slotOp.recordSlotChange(IActionMgr.OUTPUT_SLOT_ID, null, fileGroupId);
multiOp.add(slotOp);
}
/* compute/generate/move all the predecessor actions or file groups */
computeInputActions(actionId);
/*
* Store actionId/fileGroupId in our cache, to avoid doing this import again
* and ending up with multiple output file groups when only one is required.
*/
actionCache.put(actionId, fileGroupId);
/* return the ID of the output file group */
return fileGroupId;
}
/*-------------------------------------------------------------------------------------*/
/**
* For the specified action, identify (or generate) the necessary input file groups,
* which may involve recursively moving predecessor actions into our destination package.
*
* @param actionId ID of the action to be recursively dealt with.
* @throws CanNotRefactorException Something went wrong.
*/
private void computeInputActions(int actionId) throws CanNotRefactorException {
/* compute the list of input files that are read by this action */
Integer readFiles[] = actionMgr.getFilesAccessed(actionId, OperationType.OP_READ);
/* From this list of input files, we need to track the loose files, the bad files and the actions */
List<Integer> looseMembers = new ArrayList<Integer>();
List<Integer> badFiles = new ArrayList<Integer>();
List<Integer> actions = new ArrayList<Integer>();
/* for each file, figure out which action generates it, or perhaps it's a loose file? */
for (int i = 0; i < readFiles.length; i++) {
int fileId = readFiles[i];
/* if any actions "modify" (read and then write) this file, that's an error */
Integer[] modifyingActions = actionMgr.getActionsThatAccess(fileId, OperationType.OP_MODIFIED);
if (modifyingActions.length > 0) {
badFiles.add(fileId);
}
else {
/* figure out how this file is generated (if it is at all) */
Integer generatorActions[] = actionMgr.getActionsThatAccess(fileId, OperationType.OP_WRITE);
int generatorLength = generatorActions.length;
/* no generating action, so it's a "loose" file */
if (generatorLength == 0) {
looseMembers.add(fileId);
}
/* With one generator action, record that action, if it's not already recorded */
else if (generatorLength == 1) {
if (!actions.contains(generatorActions[0])) {
actions.add(generatorActions[0]);
}
}
/* files with multiple generators are a problem */
else {
badFiles.add(fileId);
}
}
}
/*
* Were there any files that were generated by multiple actions (or modified by an action).
*/
if (badFiles.size() != 0) {
throw new CanNotRefactorException(Cause.FILE_IS_MODIFIED, badFiles.toArray(new Integer[0]));
}
/*
* We now have a list of actions (with no duplicates) that are known to generate the files
* that we need as input to the current action (actionId). Recursively deal with those
* actions, and use their output file groups as our input (possibly using a merge file group
* and filters).
*/
List<Integer> fileGroups = new ArrayList<Integer>();
/* If there are any loose files, create a file group and populate it with those files */
if (looseMembers.size() > 0) {
int looseFileGroupId = createSourceFileGroup(looseMembers);
fileGroups.add(looseFileGroupId);
}
/*
* For each generating action, deal with it recursively, then record the output file group.
* By the time this loop is done, we should have all the predecessor actions/groups scheduled
* to be moved to destPkgId. The fileGroups list will contain the complete list of file
* groups generated by these sub actions.
*/
for (int subActionId : actions) {
int inputFileGroupId = moveActionToPackage(subActionId);
if (inputFileGroupId != ErrorCode.NOT_FOUND) {
if (!fileGroups.contains(inputFileGroupId)) {
fileGroups.add(inputFileGroupId);
}
}
}
/* join the upstream file group(s) to this action's input slot, if there are any groups. */
int numUpstreamFileGroups = fileGroups.size();
if (numUpstreamFileGroups >= 1) {
int inputFileGroupId;
/*
* a single input group - possibly with a filter inserted to eliminate any additional
* files that are in the group (generated by the action), but aren't required by
* the action.
*/
if (numUpstreamFileGroups == 1) {
inputFileGroupId = createFilterIfNeeded(fileGroups.get(0), readFiles);
}
/* create a merge group */
else {
inputFileGroupId = createMergeFileGroup(fileGroups, readFiles);
}
ActionUndoOp slotOp = new ActionUndoOp(buildStore, actionId);
slotOp.recordSlotChange(IActionMgr.INPUT_SLOT_ID, null, inputFileGroupId);
multiOp.add(slotOp);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a list of file IDs, create a new source file group and schedule the members to
* be added to it (by appending to the multiOp). In addition to moving the file group
* into the destination package, all the individual files are also moved. If any files
* are not within the source root, throw a {@link CanNotRefactorException} with cause
* code of PATH_OUT_OF_RANGE.
*
* @param members A list of file IDs to be added to the file group.
* @return The ID of the newly-created file group.
* @throws CanNotRefactorException Something went wrong during the refactoring.
*/
private int createSourceFileGroup(List<Integer> members) throws CanNotRefactorException {
/*
* Create the new fileGroup. Even though we won't populate it until the multiOp
* is executed, we must still allocate a new fileGroup ID number.
*/
int fileGroupId = fileGroupMgr.newSourceGroup(destPkgId);
if (fileGroupId < 0) {
throw new FatalError("Unable to create new file group");
}
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
op.recordMembershipChange(new ArrayList<Integer>(), members);
multiOp.add(op);
/*
* Move all the files into the destination package, using FileUndoOps. Before
* a file can be moved, we must ensure that it's within the source root of the package.
*/
List<Integer> filesOutOfRange = new ArrayList<Integer>();
/*
* For each loose file, validate if it's within the package roots, and if so,
* schedule an UndoOp to make the necessary change. If not, throw an exception.
*/
for (int pathId : members) {
PackageDesc oldDesc = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_FILE, pathId);
if (oldDesc == null) {
throw new FatalError("Can't find pathId");
}
/*
* Check that this path is within the source root. If so, schedule it to be move to
* the destination package.
*/
if (fileMgr.isAncestorOf(pkgRootId, pathId)) {
FileUndoOp pkgChangeOp = new FileUndoOp(buildStore, pathId);
pkgChangeOp.recordChangePackage(oldDesc.pkgId, oldDesc.pkgScopeId,
destPkgId, IPackageMemberMgr.SCOPE_PRIVATE);
multiOp.add(pkgChangeOp);
}
/*
* Else, record this pathID as being out of range. We'll report an exception
* once we've collected the complete list of invalid paths.
*/
else {
filesOutOfRange.add(pathId);
}
}
/*
* If any files were out of range, throw an exception.
*/
if (filesOutOfRange.size() > 0) {
throw new CanNotRefactorException(Cause.PATH_OUT_OF_RANGE, filesOutOfRange.toArray(new Integer[0]));
}
/* update the cache, with the members that the file group will contain (once the multiOp is executed) */
fileGroupCache.put(fileGroupId, members);
return fileGroupId;
}
/*-------------------------------------------------------------------------------------*/
/**
* Insert the specified file groups into a newly created merge file group.
*
* @param subFileGroups The sub file groups to be added.
* @param filesNeeded An array of files that are actually needed by the action that uses them.
* @return The ID of the newly-created merge file group.
*/
private int createMergeFileGroup(List<Integer> subFileGroups, Integer[] filesNeeded) {
/*
* Create the new fileGroup. Even though we won't populate it until the multiOp
* is executed, we must still allocate a new fileGroup ID number.
*/
int fileGroupId = fileGroupMgr.newMergeGroup(destPkgId);
if (fileGroupId < 0) {
throw new FatalError("Unable to create new merge file group");
}
/*
* Determine whether any filters are required (since we don't necessarily need all
* the files that appear in the subFileGroups.
*/
List<Integer> filteredSubFileGroups = new ArrayList<Integer>();
for (int subFileGroup : subFileGroups) {
filteredSubFileGroups.add(createFilterIfNeeded(subFileGroup, filesNeeded));
}
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
op.recordMembershipChange(new ArrayList<Integer>(), filteredSubFileGroups);
multiOp.add(op);
return fileGroupId;
}
/*-------------------------------------------------------------------------------------*/
/**
* @param inputFileGroupId The group ID we're planning to attach to an action's input.
* @param filesNeeded The files that are actually required by the action.
* @return If all files in the fileGroup are required, return inputFileGroupId, else
* return a filterID (which depends on inputFileGroupId) that restricts the input to
* only those files that are required.
*/
private int createFilterIfNeeded(int inputFileGroupId, Integer[] filesNeeded) {
/*
* Look through all of the files in the file group, and learn which of them are needed.
* This is an O(n*n), but for relatively small file groups (not 1000s of items), this is OK.
*/
List<Integer> fileGroupMembers = fileGroupCache.get(inputFileGroupId);
if (fileGroupMembers == null) {
throw new FatalError("Can't find members for fileGroup with ID: " + inputFileGroupId);
}
List<Integer> neededFileGroupMembers = new ArrayList<Integer>();
for (int pathId : fileGroupMembers) {
for (int i = 0; i < filesNeeded.length; i++) {
if (pathId == filesNeeded[i]) {
neededFileGroupMembers.add(pathId);
break;
}
}
}
/*
* If the set of needed members is smaller than the set of total members, we must add
* a filter so that only the required files are kept.
*/
if (neededFileGroupMembers.size() < fileGroupMembers.size()) {
int filterGroupId = fileGroupMgr.newFilterGroup(destPkgId, inputFileGroupId);
if (filterGroupId < 0) {
throw new FatalError("Unable to create new filter file group");
}
/*
* Populate the filter with patterns that match the needed paths. Note that
* the file is not (yet) in the destination package, so we just pretend its
* there.
*/
List<String> neededPatterns = new ArrayList<String>();
for (int pathId : neededFileGroupMembers) {
String pathString = fileMgr.getPathName(pathId, destPkgId);
if (pathString == null) {
throw new FatalError("Invalid pathId in file group");
}
neededPatterns.add("ia:" + pathString);
}
/* schedule the population of the filter group with patterns */
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, filterGroupId);
op.recordMembershipChange(new ArrayList<String>(), neededPatterns);
multiOp.add(op);
return filterGroupId;
}
/* else, no filter required - return the original file group */
else {
return inputFileGroupId;
}
}
/*-------------------------------------------------------------------------------------*/
}