/*******************************************************************************
* 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.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.regex.PatternSyntaxException;
import com.buildml.model.FatalBuildStoreError;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileGroupMgr;
import com.buildml.model.IFileGroupMgrListener;
import com.buildml.model.IFileMgr;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.IFileMgr.PathType;
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.utils.errors.ErrorCode;
import com.buildml.utils.regex.BmlRegex;
import com.buildml.utils.regex.RegexChain;
/**
* An implementation of {@link IFileGroupMgr}.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class FileGroupMgr implements IFileGroupMgr {
/*=====================================================================================*
* NESTED CLASSES
*=====================================================================================*/
/**
* Represents a single transient member of a file group. A file group has zero or more
* entries of this type that are only stored in memory, and are never persisted to the
* database.
*/
private class TransientEntry {
/** The String path that this entry represents */
String pathString;
}
/*=====================================================================================*
* TYPES/FIELDS
*=====================================================================================*/
/** The BuildStore that delegates work to this FileGroupMgr */
private IBuildStore buildStore;
/** This BuildStore's file mgr */
private IFileMgr fileMgr = null;
/**
* Our database manager object, used to access the database content. This is provided
* to us when the FileGroupMgr is first instantiated.
*/
private BuildStoreDB db = null;
/** The PackageMemberMgr object associated with this FileGroupMgr */
private IPackageMemberMgr pkgMemberMgr = null;
/**
* Various prepared statement for database access.
*/
private PreparedStatement
insertNewGroupPrepStmt = null,
findGroupTypePrepStmt = null,
findGroupPredPrepStmt = null,
removeGroupPrepStmt = null,
shiftUpPathsPrepStmt = null,
insertPathAtPrepStmt = null,
findGroupSizePrepStmt = null,
findPathsAtPrepStmt = null,
findIntegerMembersPrepStmt = null,
findStringMembersPrepStmt = null,
findGroupMembersPrepStmt = null,
removePathPrepStmt = null,
removePathsPrepStmt = null,
shiftDownPathsPrepStmt = null,
insertPackageMemberPrepStmt = null,
removePackageMemberPrepStmt = null,
findSourceGroupsContainingPathPrepStmt = null;
/**
* A mapping from group ID to the list of transient path entries.
*/
private HashMap<Integer, ArrayList<TransientEntry>> transientEntryMap = null;
/** The event listeners who are registered to learn about file group changes */
List<IFileGroupMgrListener> listeners = new ArrayList<IFileGroupMgrListener>();
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new FileGroupMgr.
*
* @param buildStore The BuildStore that delegates work to this FileGroupMgr.
*/
public FileGroupMgr(BuildStore buildStore) {
this.buildStore = buildStore;
this.db = buildStore.getBuildStoreDB();
this.fileMgr = buildStore.getFileMgr();
/* initialize prepared database statements */
insertNewGroupPrepStmt = db.prepareStatement(
"insert into fileGroups values (null, ?, ?)");
findGroupTypePrepStmt = db.prepareStatement(
"select type from fileGroups where id = ?");
findGroupPredPrepStmt = db.prepareStatement(
"select predId from fileGroups where id = ?");
removeGroupPrepStmt = db.prepareStatement(
"delete from fileGroups where id = ?");
shiftUpPathsPrepStmt = db.prepareStatement(
"update fileGroupPaths set pos = pos + 1 where groupId = ? and pos >= ?");
insertPathAtPrepStmt = db.prepareStatement(
"insert into fileGroupPaths values (?, ?, ?, ?)");
findGroupSizePrepStmt = db.prepareStatement(
"select count(*) from fileGroupPaths where groupId = ?");
findPathsAtPrepStmt = db.prepareStatement(
"select pathId, pathString from fileGroupPaths where groupId = ? and pos = ?");
findIntegerMembersPrepStmt = db.prepareStatement(
"select pathId from fileGroupPaths where groupId = ? order by pos");
findStringMembersPrepStmt = db.prepareStatement(
"select pathString from fileGroupPaths where groupId = ? order by pos");
findGroupMembersPrepStmt = db.prepareStatement(
"select pathId, pathString from fileGroupPaths where groupId = ? order by pos");
removePathPrepStmt = db.prepareStatement(
"delete from fileGroupPaths where groupId = ? and pos = ?");
removePathsPrepStmt = db.prepareStatement(
"delete from fileGroupPaths where groupId = ?");
shiftDownPathsPrepStmt = db.prepareStatement(
"update fileGroupPaths set pos = pos - 1 where groupId = ? and pos >= ?");
insertPackageMemberPrepStmt =
db.prepareStatement("insert into packageMembers values (?, ?, ?, ?, -1, -1)");
removePackageMemberPrepStmt =
db.prepareStatement("delete from packageMembers where memberId = ? and memberType = " +
IPackageMemberMgr.TYPE_FILE_GROUP);
findSourceGroupsContainingPathPrepStmt = db.prepareStatement(
"select distinct groupId from fileGroups, fileGroupPaths" +
" where (fileGroups.id = fileGroupPaths.groupId)" +
" and (fileGroups.type = " + IFileGroupMgr.SOURCE_GROUP + ")" +
" and (pathId = ?)");
/* initialize the mapping of group IDs to list of transient entries */
transientEntryMap = new HashMap<Integer, ArrayList<TransientEntry>>();
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getGroupType(int)
*/
@Override
public int getGroupType(int groupId) {
/* fetch the type of this group */
Integer results[] = null;
try {
findGroupTypePrepStmt.setInt(1, groupId);
results = db.executePrepSelectIntegerColumn(findGroupTypePrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
if (results.length != 0) {
return results[0];
}
return ErrorCode.NOT_FOUND;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getGroupSize(int)
*/
@Override
public int getGroupSize(int groupId) {
int type = getGroupType(groupId);
if (type == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
Integer results[] = null;
try {
findGroupSizePrepStmt.setInt(1, groupId);
results = db.executePrepSelectIntegerColumn(findGroupSizePrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
return results[0];
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getExpandedGroupFiles(int)
*/
@Override
public String[] getExpandedGroupFiles(int groupId) {
/* all group types provide us with an array of string paths */
ArrayList<String> outputPaths = new ArrayList<String>();
/* since merge groups can be recursive, we need a recursion helper */
if (getExpandedGroupFilesHelper(groupId, outputPaths) != ErrorCode.OK) {
return null;
}
/* final results as a String[] */
return outputPaths.toArray(new String[0]);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#moveEntry(int, int, int)
*/
@Override
public int moveEntry(int groupId, int fromIndex, int toIndex) {
/* validate inputs */
int size = getGroupSize(groupId);
if (size == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if ((fromIndex < 0) || (fromIndex >= size) || (toIndex < 0) || (toIndex >= size)) {
return ErrorCode.OUT_OF_RANGE;
}
/* moving to the same position is pointless */
if (fromIndex == toIndex) {
return ErrorCode.OK;
}
/* determine the ID of the path we're moving */
Object output[] = new Object[2];
if (getPathsAtHelper(groupId, fromIndex, output) == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
int pathId = (Integer)output[0];
String pathString = (String)output[1];
/* delete the old entry */
removeEntryHelper(groupId, fromIndex);
/* insert the same path at the new location */
addEntryHelper(groupId, pathId, pathString, toIndex, size);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#removeEntry(int, int)
*/
@Override
public int removeEntry(int groupId, int index) {
/* validate inputs */
int size = getGroupSize(groupId);
if (size == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if ((index < 0) || (index >= size)) {
return ErrorCode.OUT_OF_RANGE;
}
/* update the database */
removeEntryHelper(groupId, index);
/* notify listeners about the change */
notifyListeners(groupId, IFileGroupMgrListener.CHANGED_MEMBERSHIP);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#removeGroup(int)
*/
@Override
public int removeGroup(int groupId) {
/* group must exist and be empty */
int size = getGroupSize(groupId);
if (size == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if (size != 0) {
return ErrorCode.CANT_REMOVE;
}
/* update the database to remove it from fileGroups and packageMembers tables*/
try {
removeGroupPrepStmt.setInt(1, groupId);
db.executePrepUpdate(removeGroupPrepStmt);
removePackageMemberPrepStmt.setInt(1, groupId);
db.executePrepUpdate(removePackageMemberPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#newSourceGroup(int)
*/
@Override
public int newSourceGroup(int pkgId) {
return newGroup(pkgId, IFileGroupMgr.SOURCE_GROUP, -1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addSourcePath(int, int)
*/
@Override
public int addPathId(int groupId, int pathId) {
return addPathId(groupId, pathId, -1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addSourcePath(int, int, int)
*/
@Override
public int addPathId(int groupId, int pathId, int index) {
/* by fetching the size, we check if the group is valid */
int initialSize = getGroupSize(groupId);
if (initialSize == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* only applicable for source file groups */
if (getGroupType(groupId) != SOURCE_GROUP) {
return ErrorCode.INVALID_OP;
}
/* validate that the pathId is valid */
if (fileMgr.getPathType(pathId) == PathType.TYPE_INVALID) {
return ErrorCode.BAD_VALUE;
}
if (index == -1) {
index = initialSize;
} else if ((index < 0) || (index > initialSize)) {
return ErrorCode.OUT_OF_RANGE;
}
addEntryHelper(groupId, pathId, null, index, initialSize);
return index;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getSourcePathAt(int, int)
*/
@Override
public int getPathId(int groupId, int index) {
int size = getGroupSize(groupId);
if (size == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* only applicable for source file groups */
if (getGroupType(groupId) != SOURCE_GROUP) {
return ErrorCode.INVALID_OP;
}
if ((index < 0) || (index >= size)) {
return ErrorCode.OUT_OF_RANGE;
}
Object output[] = new Object[2];
if (getPathsAtHelper(groupId, index, output) == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
return (Integer)output[0];
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getPathIds(int)
*/
@Override
public Integer[] getPathIds(int groupId) {
/* only applicable for merge file groups */
int groupType = getGroupType(groupId);
if (groupType != SOURCE_GROUP) {
return null;
}
/* defer work to our helper */
return getIntegerMembersHelper(groupId);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#setPathIds(int, java.lang.Integer[])
*/
@Override
public int setPathIds(int groupId, Integer[] members) {
/* only applicable for source file groups */
int groupType = getGroupType(groupId);
if (groupType == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if (groupType != SOURCE_GROUP) {
return ErrorCode.INVALID_OP;
}
setMembersHelper(groupId, members);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getSourceGroupsContainingPath(int)
*/
@Override
public Integer[] getSourceGroupsContainingPath(int pathId) {
Integer results[] = null;
try {
findSourceGroupsContainingPathPrepStmt.setInt(1, pathId);
results = db.executePrepSelectIntegerColumn(findSourceGroupsContainingPathPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
return results;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#newMergeGroup(int)
*/
@Override
public int newMergeGroup(int pkgId) {
return newGroup(pkgId, IFileGroupMgr.MERGE_GROUP, -1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addSubGroup(int, int)
*/
@Override
public int addSubGroup(int groupId, int subGroupId) {
return addSubGroup(groupId, subGroupId, -1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addSubGroup(int, int, int)
*/
@Override
public int addSubGroup(int groupId, int subGroupId, int index) {
/* by fetching the size, we check if the group is valid */
int initialSize = getGroupSize(groupId);
if (initialSize == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* only applicable for merge file groups */
if (getGroupType(groupId) != MERGE_GROUP) {
return ErrorCode.INVALID_OP;
}
/* validate that the sub group ID is valid */
if (getGroupSize(subGroupId) == ErrorCode.NOT_FOUND) {
return ErrorCode.BAD_VALUE;
}
if (index == -1) {
index = initialSize;
} else if ((index < 0) || (index > initialSize)) {
return ErrorCode.OUT_OF_RANGE;
}
/*
* Check for possibility of cycles.
*/
if ((groupId == subGroupId) ||
checkForCycles(IPackageMemberMgr.TYPE_FILE_GROUP, groupId, subGroupId)) {
return ErrorCode.LOOP_DETECTED;
}
addEntryHelper(groupId, subGroupId, null, index, initialSize);
return index;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getSubGroupAt(int, int)
*/
@Override
public int getSubGroup(int groupId, int index) {
int size = getGroupSize(groupId);
if (size == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* only applicable for merge file groups */
if (getGroupType(groupId) != MERGE_GROUP) {
return ErrorCode.INVALID_OP;
}
if ((index < 0) || (index >= size)) {
return ErrorCode.OUT_OF_RANGE;
}
Object output[] = new Object[2];
if (getPathsAtHelper(groupId, index, output) == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
return (Integer)output[0];
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getSubGroups(int)
*/
@Override
public Integer[] getSubGroups(int groupId) {
/* only applicable for merge file groups */
int groupType = getGroupType(groupId);
if (groupType != MERGE_GROUP) {
return null;
}
/* delegate most of the work to a helper */
return getIntegerMembersHelper(groupId);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#setSubGroups(int, java.lang.Integer[])
*/
@Override
public int setSubGroups(int groupId, Integer[] members) {
/* only applicable for merge file groups */
int groupType = getGroupType(groupId);
if (groupType == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if (groupType != MERGE_GROUP) {
return ErrorCode.INVALID_OP;
}
setMembersHelper(groupId, members);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#newGeneratedGroup(int)
*/
@Override
public int newGeneratedGroup(int pkgId) {
return newGroup(pkgId, IFileGroupMgr.GENERATED_GROUP, -1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#newFilterGroup(int, int)
*/
@Override
public int newFilterGroup(int pkgId, int predGroupId) {
/* check that pkgId refers to a valid package */
IPackageMgr pkgMgr = buildStore.getPackageMgr();
if (!pkgMgr.isValid(pkgId)) {
return ErrorCode.NOT_FOUND;
}
/* validate the predGroupId exists and is already in the pkgId package */
if (getGroupType(predGroupId) == ErrorCode.NOT_FOUND) {
return ErrorCode.BAD_VALUE;
}
pkgMemberMgr = buildStore.getPackageMemberMgr();
PackageDesc desc = pkgMemberMgr.getPackageOfMember(IPackageMemberMgr.TYPE_FILE_GROUP, predGroupId);
if ((desc == null) || (desc.pkgId != pkgId)) {
return ErrorCode.BAD_VALUE;
}
/* defer the rest of the work - it's common across group types */
return newGroup(pkgId, IFileGroupMgr.FILTER_GROUP, predGroupId);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getPredId(int)
*/
@Override
public int getPredId(int groupId) {
/* validate that groupId is valid group */
int groupType = getGroupType(groupId);
if (groupType == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* only applicable for filter file groups */
if (groupType != FILTER_GROUP) {
return ErrorCode.INVALID_OP;
}
Integer results[] = null;
try {
findGroupPredPrepStmt.setInt(1, groupId);
results = db.executePrepSelectIntegerColumn(findGroupPredPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
if (results.length != 0) {
return results[0];
}
return ErrorCode.NOT_FOUND;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addGeneratedPath(int, java.lang.String, boolean)
*/
@Override
public int addPathString(int groupId, String path) {
return addPathString(groupId, path, -1);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addGeneratedPath(int, java.lang.String, boolean, int)
*/
@Override
public int addPathString(int groupId, String path, int index) {
/* by fetching the size, we check if the group is valid */
int initialSize = getGroupSize(groupId);
if (initialSize == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* only applicable for generated file groups */
int groupType = getGroupType(groupId);
if ((groupType != GENERATED_GROUP) && (groupType != FILTER_GROUP)) {
return ErrorCode.INVALID_OP;
}
/* validate that the path string is valid */
if ((groupType == GENERATED_GROUP) && (!isValidPathString(path))) {
return ErrorCode.BAD_VALUE;
}
if (index == -1) {
index = initialSize;
} else if ((index < 0) || (index > initialSize)) {
return ErrorCode.OUT_OF_RANGE;
}
addEntryHelper(groupId, 0, path, index, initialSize);
return index;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getGeneratedPathAt(int, int)
*/
@Override
public String getPathString(int groupId, int index) {
/* only applicable for generated and filter groups */
int groupType = getGroupType(groupId);
if ((groupType != GENERATED_GROUP) && (groupType != FILTER_GROUP)) {
return null;
}
int size = getGroupSize(groupId);
if ((index < 0) || (index >= size)) {
return null;
}
Object output[] = new Object[2];
if (getPathsAtHelper(groupId, index, output) == ErrorCode.NOT_FOUND) {
return null;
}
return (String)output[1];
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getPathStrings(int)
*/
@Override
public String[] getPathStrings(int groupId) {
/* only applicable for generated and filter groups */
int groupType = getGroupType(groupId);
if ((groupType != GENERATED_GROUP) && (groupType != FILTER_GROUP)) {
return null;
}
/* Defer the work to our helper */
return getStringMembersHelper(groupId);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#setPathStrings(int, java.lang.String[])
*/
@Override
public int setPathStrings(int groupId, String[] members) {
/* only applicable for generated and filter groups */
int groupType = getGroupType(groupId);
if (groupType == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if ((groupType != GENERATED_GROUP) && (groupType != FILTER_GROUP)) {
return ErrorCode.INVALID_OP;
}
if (members == null) {
return ErrorCode.BAD_VALUE;
}
/* defer to our helper function */
setMembersHelper(groupId, members);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addTransientPathString(int, java.lang.String)
*/
@Override
public int addTransientPathString(int groupId, String path) {
/* only generated groups are supported */
int type = getGroupType(groupId);
if (type == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if (type != GENERATED_GROUP) {
return ErrorCode.INVALID_OP;
}
/* validate that the path string is valid */
if (!isValidPathString(path)) {
return ErrorCode.BAD_VALUE;
}
Integer groupIdInt = Integer.valueOf(groupId);
/*
* Check if there are already transient paths for this group. If not, create
* a new entry for this group.
*/
ArrayList<TransientEntry> entries = transientEntryMap.get(groupIdInt);
if (entries == null) {
entries = new ArrayList<FileGroupMgr.TransientEntry>();
transientEntryMap.put(groupIdInt, entries);
}
/* append the new path to the entry list */
TransientEntry newEntry = new TransientEntry();
newEntry.pathString = path;
entries.add(newEntry);
/* notify listeners about the change */
notifyListeners(groupId, IFileGroupMgrListener.CHANGED_MEMBERSHIP);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#clearTransientPathStrings(int)
*/
@Override
public int clearTransientPathStrings(int groupId) {
/* only generated groups are supported */
int type = getGroupType(groupId);
if (type == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
if (type != GENERATED_GROUP) {
return ErrorCode.INVALID_OP;
}
transientEntryMap.remove(Integer.valueOf(groupId));
/* notify listeners about the change */
notifyListeners(groupId, IFileGroupMgrListener.CHANGED_MEMBERSHIP);
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#newSubPackageGroup(int, int, int)
*/
@Override
public int newSubPackageGroup(int subPkgId, int slotId) {
// TODO Auto-generated method stub
return 0;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getSubPkgId(int)
*/
@Override
public int getSubPkgId(int groupId) {
// TODO Auto-generated method stub
return 0;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getSubPkgSlotId(int)
*/
@Override
public int getSubPkgSlotId(int groupId) {
// TODO Auto-generated method stub
return 0;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#getBuildStore()
*/
@Override
public IBuildStore getBuildStore() {
return buildStore;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#addListener(com.buildml.model.IFileGroupMgrListener)
*/
@Override
public void addListener(IFileGroupMgrListener listener) {
listeners.add(listener);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileGroupMgr#removeListener(com.buildml.model.IFileGroupMgrListener)
*/
@Override
public void removeListener(IFileGroupMgrListener listener) {
listeners.remove(listener);
};
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Helper method for creating a new group. This is only invoked by public methods
* such as newSourceGroup(), newMergeGroup(), etc.
*
* @param pkgId ID of the package to add the group to.
* @param type The type of the new group (SOURCE, GENERATED, etc)
* @param optArg1 Some group types contain an additional argument.
* @return The new group's ID.
*/
private int newGroup(int pkgId, int type, int optArg1) {
int lastRowId;
/* validate inputs */
if ((type < IFileGroupMgr.SOURCE_GROUP) ||
(type > IFileGroupMgr.FILTER_GROUP)) {
return ErrorCode.BAD_VALUE;
}
IPackageMgr pkgMgr = buildStore.getPackageMgr();
if (!pkgMgr.isValid(pkgId)) {
return ErrorCode.NOT_FOUND;
}
/* insert the new group into the database, returning the new group ID */
try {
insertNewGroupPrepStmt.setInt(1, type);
insertNewGroupPrepStmt.setInt(2, optArg1);
db.executePrepUpdate(insertNewGroupPrepStmt);
lastRowId = db.getLastRowID();
/* insert the default package membership values */
insertPackageMemberPrepStmt.setInt(1, IPackageMemberMgr.TYPE_FILE_GROUP);
insertPackageMemberPrepStmt.setInt(2, lastRowId);
insertPackageMemberPrepStmt.setInt(3, pkgId);
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("Error in SQL: " + e);
}
return lastRowId;
}
/*-------------------------------------------------------------------------------------*/
/**
* Fetch the pathId and pathString values for a specific (groupId/index) combination.
* No error checking is done, so the caller must have first validated all inputs.
*
* @param groupId The ID of the group to query.
* @param index The index of the path within the group.
* @param output An array of two elements, for returning the results. On return from
* this method (with a ErrorCode.OK return code), output[0] will
* contain the Integer pathId, and output[1] will contain the String
* path string (possibly null). If the return code is not ErrorCode.OK,
* this array is untouched.
*
* @return ErrorCode.OK on success, or ErrorCode.NOT_FOUND if the
*/
private int getPathsAtHelper(int groupId, int index, Object output[]) {
ResultSet rs = null;
try {
findPathsAtPrepStmt.setInt(1, groupId);
findPathsAtPrepStmt.setInt(2, index);
rs = db.executePrepSelectResultSet(findPathsAtPrepStmt);
/* this shouldn't happen (groups should be complete), but just in case... */
if (!rs.next()) {
return ErrorCode.NOT_FOUND;
}
output[0] = rs.getInt(1);
output[1] = rs.getString(2);
rs.close();
return ErrorCode.OK;
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Removed a path entry from the specified group index. There is no error checking done
* by this method, so the caller must take care to validate inputs.
*
* @param groupId The ID of the group to remove the entry from.
* @param index The index of the entry to remove.
*/
private void removeEntryHelper(int groupId, int index) {
/* first, delete the specified entry */
try {
removePathPrepStmt.setInt(1, groupId);
removePathPrepStmt.setInt(2, index);
db.executePrepUpdate(removePathPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
/* if necessary, move the higher-numbers entries down one index number */
try {
shiftDownPathsPrepStmt.setInt(1, groupId);
shiftDownPathsPrepStmt.setInt(2, index);
db.executePrepUpdate(shiftDownPathsPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Add a new path entry (with pathId and pathString), at the specified index. There
* is no error checking done by this method, so the caller must have already validated
* inputs.
*
* @param groupId The ID of the group we're modifying.
* @param pathId The path ID to be added.
* @param pathString The path string to be added (can be null).
* @param index The index at which the path will be added.
* @param size The size of the group.
*/
private void addEntryHelper(int groupId, int pathId, String pathString, int index, int size) {
/* if necessary, move existing entries up one index level */
if (index < size) {
try {
shiftUpPathsPrepStmt.setInt(1, groupId);
shiftUpPathsPrepStmt.setInt(2, index);
db.executePrepUpdate(shiftUpPathsPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/* now insert the new record at the required index */
try {
insertPathAtPrepStmt.setInt(1, groupId);
insertPathAtPrepStmt.setInt(2, pathId);
insertPathAtPrepStmt.setString(3, pathString);
insertPathAtPrepStmt.setInt(4, index);
db.executePrepUpdate(insertPathAtPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
/* notify listeners about the change */
notifyListeners(groupId, IFileGroupMgrListener.CHANGED_MEMBERSHIP);
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether a path string is valid (that is, does it start with a valid root
* name).
*
* @param path The path string to validate.
* @return True if the path is valid, else false.
*/
private boolean isValidPathString(String path) {
/* syntax must be "@<root-name>/.*" where <root-name> must be at least 4 characters */
if (!path.startsWith("@")) {
return false;
}
int slashIndex = path.indexOf('/');
if (slashIndex < "@root".length()) {
return false;
}
String rootName = path.substring(1, slashIndex);
IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();
return pkgRootMgr.getRootNative(rootName) != null;
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method for getExpandedGroupFiles(). This method handles the recursion that's
* possible when merge groups contain sub-groups. It's also possible that merge groups
* contain other merge groups.
*
* @param groupId The ID of the top-level group.
* @param outputPaths An input/output list that we'll append output paths to.
* @return ErrorCode.OK on success, ErrorCode.NOT_FOUND if a sub group is invalid,
* or ErrorCode.BAD_VALUE if it's a filter group with bad regular expressions.
*/
private int getExpandedGroupFilesHelper(int groupId, ArrayList<String> outputPaths) {
/* must be source, generated or merge group */
int type = getGroupType(groupId);
if (type == ErrorCode.NOT_FOUND) {
return ErrorCode.NOT_FOUND;
}
/* source and generate groups are simple - just expand their content */
if ((type == SOURCE_GROUP) || (type == GENERATED_GROUP)) {
/* fetch the individual members from the database */
ResultSet rs = null;
try {
findGroupMembersPrepStmt.setInt(1, groupId);
rs = db.executePrepSelectResultSet(findGroupMembersPrepStmt);
while (rs.next()) {
if (type == SOURCE_GROUP) {
int pathId = rs.getInt(1);
outputPaths.add(fileMgr.getPathName(pathId, true));
} else {
outputPaths.add(rs.getString(2));
}
}
rs.close();
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/* merge groups involve using recursion to expand children */
else if (type == MERGE_GROUP) {
int size = getGroupSize(groupId);
int index = 0;
while (index != size) {
int subGroupId = getSubGroup(groupId, index);
int rc = getExpandedGroupFilesHelper(subGroupId, outputPaths);
if (rc != ErrorCode.OK) {
return rc;
}
index++;
}
}
/* filter groups require us to filter out the files of our upstream file group */
else if (type == FILTER_GROUP) {
int predGroupId = getPredId(groupId);
if (predGroupId < 0) {
return predGroupId;
}
/* fetch the input files from the predecessor group - we'll filter from these */
String inputPaths[] = getExpandedGroupFiles(predGroupId);
/* our own file group contains the regex strings to filter with */
ResultSet rs = null;
ArrayList<String> regexs = new ArrayList<String>();
try {
findGroupMembersPrepStmt.setInt(1, groupId);
rs = db.executePrepSelectResultSet(findGroupMembersPrepStmt);
while (rs.next()) {
regexs.add(rs.getString(2));
}
rs.close();
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
String regexArray[] = regexs.toArray(new String[regexs.size()]);
/* convert the regex strings into a RegexChain (precompiled regexes) */
RegexChain chain;
try {
chain = BmlRegex.compileRegexChain(regexArray);
} catch (PatternSyntaxException ex) {
return ErrorCode.BAD_VALUE;
}
/* finally, filter the inputPaths using our regexes */
String filteredPaths[] = BmlRegex.filterRegexChain(inputPaths, chain);
for (int i = 0; i < filteredPaths.length; i++) {
outputPaths.add(filteredPaths[i]);
}
}
/* else, it's an error - unsupported group type */
else {
throw new FatalBuildStoreError("Unsupported file group type: " + type);
}
/* if there are transient entries for this group, append them to the list */
if (type == GENERATED_GROUP) {
ArrayList<TransientEntry> entries = transientEntryMap.get(Integer.valueOf(groupId));
if (entries != null) {
for (Iterator<TransientEntry> iterator = entries.iterator(); iterator.hasNext();) {
TransientEntry transientEntry = (TransientEntry) iterator.next();
outputPaths.add(transientEntry.pathString);
}
}
}
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method for determining whether a merge file group contains (somewhere in the
* hierarchy) a second group.
*
* @param parentGroupId The ID of the file group that may contain the child group.
* @param childGroupId The ID of the file group that may be contained by the parent group.
* @return True if the parent group contains the child group.
*/
private boolean groupContainsGroup(int parentGroupId, int childGroupId) {
/* recursion termination */
if (parentGroupId == childGroupId) {
return true;
}
/* if parent is a merge group, traverse recursively */
int parentType = getGroupType(parentGroupId);
if (parentType != MERGE_GROUP) {
return false;
}
int size = getGroupSize(parentGroupId);
int index = 0;
while (index != size) {
int memberGroup = getSubGroup(parentGroupId, index);
if (groupContainsGroup(memberGroup, childGroupId)) {
return true;
}
index++;
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Notify any registered listeners about our change in state.
* @param fileGroupId The file group that has changed.
* @param how The way in which the file group changed (see {@link IFileGroupMgrListener}).
*/
private void notifyListeners(int fileGroupId, int how) {
/*
* Make a copy of the listeners list, otherwise a registered listener can't remove
* itself from the list within the fileGroupChangeNotification() method.
*/
IFileGroupMgrListener listenerCopy[] =
listeners.toArray(new IFileGroupMgrListener[listeners.size()]);
for (int i = 0; i < listenerCopy.length; i++) {
listenerCopy[i].fileGroupChangeNotification(fileGroupId, how);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* If we're modifying the membership of a merge file group,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 right" in search
* of the file group to be added. For example, if we're inserting fileGroup1 into fileGroup2
* (i.e. its being inserted to the left), search downstream (right) of fileGroup2 to see if
* we bump into fileGroup1.
*
* @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 merge
* file group).
* @param fileGroupId The ID of the file group that's being inserted into the merge file group.
* @return True if a cycle would be created, else false.
*/
private boolean checkForCycles(int memberType, int memberId, int fileGroupId) {
pkgMemberMgr = buildStore.getPackageMemberMgr();
/* get neighbours of this member */
MemberDesc[] neighbours = pkgMemberMgr.getNeighboursOf(
memberType, memberId, IPackageMemberMgr.NEIGHBOUR_RIGHT, 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)) {
return true;
}
/* now loop to the next neighbour */
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper function (supporting getPathIds() and getSubGroups() to return the IDs of all
* members within a specific group.
*
* @param groupId The ID of the group to query.
* @return A (possibly empty) array of group members.
*/
private Integer[] getIntegerMembersHelper(int groupId) {
try {
findIntegerMembersPrepStmt.setInt(1, groupId);
return db.executePrepSelectIntegerColumn(findIntegerMembersPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper function (supporting getPathStrings()) to return the Strings within a file group.
*
* @param groupId The ID of the group to query.
* @return A (possibly empty) array of group members.
*/
private String[] getStringMembersHelper(int groupId) {
try {
findStringMembersPrepStmt.setInt(1, groupId);
return db.executePrepSelectStringColumn(findStringMembersPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper function (supporting setPathIds() and setSubGroups()) to set the members of
* a group. All existing members will first be removed.
*
* @param groupId The ID of the group (source or merge group that uses numeric member IDs).
* @param members The members to set.
*/
private void setMembersHelper(int groupId, Object[] members) {
/* lots of individual changes here - do them without committing */
boolean prevState = db.setFastAccessMode(true);
/* start by removing all existing members of this group */
try {
removePathsPrepStmt.setInt(1, groupId);
db.executePrepUpdate(removePathsPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
/* add all the new members */
for (int i = 0; i < members.length; i++) {
try {
insertPathAtPrepStmt.setInt(1, groupId);
if (members[i] instanceof Integer) {
insertPathAtPrepStmt.setInt(2, (Integer)members[i]);
insertPathAtPrepStmt.setString(3, null);
} else {
insertPathAtPrepStmt.setInt(2, 0);
insertPathAtPrepStmt.setString(3, (String)members[i]);
}
insertPathAtPrepStmt.setInt(4, i);
db.executePrepUpdate(insertPathAtPrepStmt);
} catch (SQLException e) {
throw new FatalBuildStoreError("Error in SQL: " + e);
}
}
/* commit */
db.setFastAccessMode(prevState);
/* notify about the change */
notifyListeners(groupId, IFileGroupMgrListener.CHANGED_MEMBERSHIP);
}
/*-------------------------------------------------------------------------------------*/
/**
* TODO: complete this as a helper
* @param groupId
* @return files
*/
public String[] getExpandedGroupFilesViaSubPackage(int groupId) {
return null;
}
/*-------------------------------------------------------------------------------------*/
}