/*******************************************************************************
* 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.scanner.electricanno;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.buildml.model.IActionMgr;
import com.buildml.model.IActionMgr.OperationType;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileMgr;
import com.buildml.model.IPackageRootMgr;
import com.buildml.scanner.FatalBuildScannerError;
import com.buildml.utils.errors.ErrorCode;
import com.buildml.utils.string.PathUtils;
/**
* A SAX Handler class used when parsing an Electric Accelerator annotation file. The
* XML elements in the file are parsed, and the associated data is added into the
* BuildStore. The most relevant parts of the XML structure are:
* <p>
* <job type="..."><br>
* ...<br>
* <op type="..." file="..." /><br>
* ...<br>
* </job><br>
* <p>
* For each job, we extract the details of each file access (read, write or create), along
* with the file's name.
*
* @author "Peter Smith <psmith@arapiki.com>"
*/
/* package */ class ElectricAnnoSAXHandler extends DefaultHandler {
/*=====================================================================================*
* TYPES/FIELDS
*=====================================================================================*/
/** The command argv StringBuffer starts out this size. */
private static final int INITIAL_COMMAND_SB_SIZE = 256;
/**
* Set if we're parsing the content within a <job>...</job> pair of elements. This is used
* for optimizing the decision making in our code.
*/
private boolean withinJob = false;
/** Set if we're parsing the content within a <argv>...</argv> pair of elements. */
private boolean withinArgv = false;
/**
* The command line for this job. This is a StringBuffer, since command lines are often
* multiline and we append to them as we seen new <argv> tags.
*/
StringBuffer commandArgv;
/** The ActionMgr object associated with this BuildStore. */
private IActionMgr actionMgr;
/** The FileMgr object associated with this BuildStore. */
private IFileMgr fileMgr;
/** The PackageRootMgr object associated with this BuildStore. */
private IPackageRootMgr pkgRootMgr;
/** The set of files that have been read in the current <job>. */
private ArrayList<String> filesRead;
/** The set of files that have been written in the current <job>. */
private ArrayList<String> filesWritten;
/** The ID of the current action's parent action. */
private int currentParentAction;
/** The ID of the action we most recently processed. */
private int mostRecentAction;
/** Our stack of parent actions, recording our progress through the <make> </make> tags. */
private ArrayList<Integer> actionStack;
/** The file system directory in which the current action was performed. */
private int currentDirId;
/** Maintain a stack of directories, pushing and popping as we encounter <make> and </make>. */
private ArrayList<Integer> directoryStack;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Instantiate a new ElectricAnnoSAXHandler object, with a reference
* to the BuildStore to be populated.
*
* @param buildStore The BuildStore object to be populated.
*/
public ElectricAnnoSAXHandler(IBuildStore buildStore) {
actionMgr = buildStore.getActionMgr();
fileMgr = buildStore.getFileMgr();
pkgRootMgr = buildStore.getPackageRootMgr();
/* create a stack to remember the hierarchy of actions */
actionStack = new ArrayList<Integer>();
/* To start with, all actions we encounter are children of the root action */
mostRecentAction = currentParentAction = actionMgr.getRootAction("");
actionStack.add(currentParentAction);
/* we'll also need to track our current directory */
directoryStack = new ArrayList<Integer>();
directoryStack.add(pkgRootMgr.getRootPath("root"));
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Handle occurrences of an element start tag. This method is invoked by the SAX Parser
* whenever a new element start tag (e.g. <job>) is identified. See the definition of
* import org.xml.sax.helpers.DefaultHandler for parameter details.
*/
@Override
public void startElement(String uri, String localName, String qName,
Attributes atts) throws SAXException {
/*
* The <job> element contains the most interesting data, we'll set "withinJob" to true
* if we're within a <job> </job> pair.
*/
if (localName.equals("job")) {
/* we only care about jobs of type "parse" or "rule". */
String jobType = atts.getValue("type");
if ((jobType != null) && (jobType.equals("parse") || jobType.equals("rule"))){
withinJob = true;
/*
* Create an empty pair of sets for recording files that this job has read,
* or that this job has written.
*/
filesRead = new ArrayList<String>();
filesWritten = new ArrayList<String>();
}
}
/*
* else, the <make> tag tells us the nesting relationship between actions.
*/
else if (localName.equals("make")) {
String cwd = atts.getValue("cwd");
/* record the <make>'s current directory */
currentDirId = fileMgr.addDirectory(cwd);
if (currentDirId == ErrorCode.BAD_PATH) {
throw new FatalBuildScannerError("Unable to register new directory in database: " + cwd);
}
directoryStack.add(currentDirId);
/* push our existing state on a stack, effectively changing our current parent action */
actionStack.add(Integer.valueOf(mostRecentAction));
currentParentAction = mostRecentAction;
}
/*
* If we're within a <job> </job> pair, do some extra processing.
*/
if (withinJob) {
/*
* We want to know which files are accesses. These are specified
* within the <op> element.
*/
if (localName.equals("op")) {
String opType = atts.getValue("type");
String file = atts.getValue("file");
String isDir = atts.getValue("isdir");
file = PathUtils.normalizeAbsolutePath(file);
/* for files (not directories) that are read or written... */
if ((opType != null) && (isDir == null)) {
/* this file was read by the job */
if (opType.equals("read")){
filesRead.add(file);
}
/* this file was written by the job */
else if (opType.equals("create")) {
filesWritten.add(file);
}
/* this file was renamed by the job */
else if (opType.equals("rename")) {
String otherFile = atts.getValue("other");
otherFile = PathUtils.normalizeAbsolutePath(otherFile);
filesWritten.remove(otherFile);
filesWritten.add(file);
// TODO: does this work if otherFile was added multiple times?
}
}
}
/* else, handle command line arguments that appear within <argv> </argv> */
else if (localName.equals("argv")) {
withinArgv = true;
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Handle occurrences of an element end tag. This method is invoked by the SAX Parser
* whenever a new element end tag (e.g. </job>) is identified. See the definition of
* import org.xml.sax.helpers.DefaultHandler for parameter details.
*/
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
/*
* If we've found a </job> element tag, so we've parsed all the information related
* to this job. To finish up, create a new build action and populate it with all the
* file access information we acquired..
*/
if (withinJob && localName.equals("job")) {
withinJob = false;
/*
* We've seen all the details of this job, and if it's an interesting
* job, then we'll add it as a build action. We currently define "interesting"
* as whether there's a command argv associated with it.
*/
if (commandArgv != null){
String argvString = commandArgv.toString();
commandArgv = null;
int newActionId = actionMgr.addShellCommandAction(currentParentAction, currentDirId, argvString);
/* record the ID of this action, since it might be the new parent action soon */
mostRecentAction = newActionId;
/* add all the file reads to the build action */
for (String file : filesRead) {
int newFileId = fileMgr.addFile(file);
if (newFileId != ErrorCode.BAD_PATH) {
actionMgr.addFileAccess(newActionId, newFileId, OperationType.OP_READ);
} else {
throw new FatalBuildScannerError("Unable to register new file in database: " + file);
}
}
/* add all the file writes to the build action */
for (String file : filesWritten) {
int newFileId = fileMgr.addFile(file);
if (newFileId != ErrorCode.BAD_PATH) {
actionMgr.addFileAccess(newActionId, newFileId, OperationType.OP_WRITE);
} else {
throw new FatalBuildScannerError("Unable to register new file in database: " + file);
}
}
}
}
/* have we seen a </argv>? */
else if (withinArgv && localName.equals("argv")) {
withinArgv = false;
if (commandArgv != null) {
commandArgv.append('\n');
}
}
/* else, we're done with this nesting level of actions */
else if (localName.equals("make")) {
/*
* Restore the current parent's ID and most recent action ID for the
* parent's <job>
*/
int actionStackSize = actionStack.size();
if (actionStackSize == 0) {
throw new FatalBuildScannerError("Too many </make> tags in annotation file");
}
currentParentAction = actionStack.get(actionStackSize - 2);
mostRecentAction = actionStack.remove(actionStackSize - 1);
/* restore the previous job's current directory */
int dirStackSize = directoryStack.size();
directoryStack.remove(dirStackSize - 1);
currentDirId = directoryStack.get(dirStackSize - 2);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Handle occurrences of raw character in an XML stream. That is, the text that appears
* between the start and end tags. For example, <job>this is the text</job>.
* See the definition of import org.xml.sax.helpers.DefaultHandler for parameter details.
*/
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
if (withinArgv) {
if (commandArgv == null) {
commandArgv = new StringBuffer(INITIAL_COMMAND_SB_SIZE);
}
commandArgv.append(ch, start, length);
}
}
/*-------------------------------------------------------------------------------------*/
}