package hudson.plugins.coverage.model;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* An instance of an {@linkplain Element}.
*
* @author Stephen Connolly
* @since 26-Jun-2008 21:45:31
*/
public class Instance {
// ------------------------------ FIELDS ------------------------------
/**
* The parent instance.
*/
private final Instance parent;
/**
* The instance name.
*/
private final String name;
/**
* The instance element type.
*/
private final Element element;
/**
* The instance's children.
*/
private final Map<Element, Map<String, Instance>> children =
new TreeMap<Element, Map<String, Instance>>();
/**
* The instance's measurements.
*/
private final Map<Metric, Measurement> measurements = new HashMap<Metric, Measurement>();
/**
* Used to keep track of multiple recorders mapping the same file to different models. Only set for file level
* elements while on the slave that hosts the source code.
*/
private transient File sourceFile;
/**
* Used to keep track of all the source code files so we can archive the source code. Only set for the root element
* while on the slave that hosts the source code.
*/
private final transient Set<File> sourceFiles;
/**
* Used to keep track of all the mesurement files, memo objects and their associated recorders.
*/
private final transient Map<Recorder, Map<File, Collection<Object>>> measurementFiles;
// -------------------------- STATIC METHODS --------------------------
/**
* Creates a new Instance instance using the provided recorders.
*
* @param recorders The recorders.
*
* @return The new Instance instance.
*/
public static Instance newInstance(Set<? extends Recorder> recorders) {
Instance result = new Instance();
// first-pass: identify the source files
for (Recorder recorder : recorders) {
recorder.identifySourceFiles(result);
}
// second-pass: parse the coverage results
result.parseSourceResults();
// third-pass: consolidate the results
result.applyModels();
// done
return result;
}
/**
* Apply the model to the results
*/
private void applyModels() {
for (Element child : element.getChildren()) {
for (Instance childInstance : getChildren(child).values()) {
childInstance.applyModels();
}
}
element.getModel().apply(this);
}
/**
* Second-pass parsing.
*/
private synchronized void parseSourceResults() {
if (element.isSubfileLevel()) {
throw new IllegalStateException("Should never get here");
}
if (element.isFileLevel()) {
for (Map.Entry<Recorder, Map<File, Collection<Object>>> source : measurementFiles.entrySet()) {
Recorder recorder = source.getKey();
for (Map.Entry<File, Collection<Object>> observation : source.getValue().entrySet()) {
recorder.parseSourceResults(this, observation.getKey(), observation.getValue());
}
}
measurementFiles.clear();
} else {
for (Element child : element.getChildren()) {
for (Instance childInstance : getChildren(child).values()) {
childInstance.parseSourceResults();
}
}
}
}
// --------------------------- CONSTRUCTORS ---------------------------
/**
* Constructs a new root Instance instance.
*/
private Instance() {
this.element = Element.getRootElement();
this.name = "";
this.parent = null;
for (Element child : this.element.getChildren()) {
children.put(child, Collections.synchronizedMap(new TreeMap<String, Instance>()));
}
this.sourceFile = null;
this.sourceFiles = new HashSet<File>();
this.measurementFiles = new HashMap<Recorder, Map<File, Collection<Object>>>();
}
/**
* Constructs a new Instance instance.
*
* @param element The element type of the new instance.
* @param parent The parent instance.
* @param name The name of the instance.
*/
private Instance(Element element, Instance parent, String name) {
element.getClass(); // throw NPE if null
parent.getClass(); // throw NPE if null
name.getClass(); // throw NPE if null
if (element.isFileLevel()) {
throw new IllegalArgumentException("You must specify the source file for a file level element");
}
this.element = element;
this.name = name;
this.parent = parent;
this.sourceFile = null;
for (Element child : element.getChildren()) {
children.put(child, Collections.synchronizedMap(new TreeMap<String, Instance>()));
}
this.sourceFiles = null;
this.measurementFiles = null;
}
/**
* Constructs a new Instance instance at the file level.
*
* @param element The element type of the new instance.
* @param parent The parent instance.
* @param name The name of the instance.
* @param sourceFile The source code that this file element corresponds to.
*/
private Instance(Element element, Instance parent, String name, File sourceFile) {
element.getClass(); // throw NPE if null
parent.getClass(); // throw NPE if null
name.getClass(); // throw NPE if null
sourceFile.getClass(); // throw NPE if null
if (!element.isFileLevel()) {
throw new IllegalArgumentException("You can only specify the source file for a file level element");
}
this.element = element;
this.name = name;
this.parent = parent;
this.sourceFile = sourceFile;
for (Element child : element.getChildren()) {
children.put(child, Collections.synchronizedMap(new TreeMap<String, Instance>()));
}
this.sourceFiles = null;
this.measurementFiles = new HashMap<Recorder, Map<File, Collection<Object>>>();
}
// --------------------- GETTER / SETTER METHODS ---------------------
/**
* Getter for property 'parent'.
*
* @return Value for property 'parent'.
*/
public Instance getParent() {
return parent;
}
/**
* Getter for property 'name'.
*
* @return Value for property 'name'.
*/
public String getName() {
return name;
}
/**
* Returns the element type of this instance.
*
* @return the element type of this instance.
*/
public Element getElement() {
return element;
}
// -------------------------- OTHER METHODS --------------------------
/**
* Registers a recorder.
*
* @param recorder The recorder.
* @param measurementFile The measurement files.
* @param memo A memo object that the recorder can use to hold state prior to the second-pass parsing
* {@linkplain Recorder#parseSourceResults(Instance,java.io.File, Collection)}
*
* @see hudson.plugins.coverage.model.Recorder#identifySourceFiles(Instance) the first-pass parsing which should
* register recorders.
* @see hudson.plugins.coverage.model.Recorder#reidentifySourceFiles(Instance, java.util.Set, java.io.File) the
* first-pass parsing at report time which should register recorders.
* @see Recorder#parseSourceResults(Instance,java.io.File, Collection) the second-pass parsing which populates the
* parse results.
*/
public synchronized void addRecorder(Recorder recorder, File measurementFile, Object memo) {
recorder.getClass(); // throw NPE if null
measurementFile.getClass(); // throw NPE if null
if (!element.isFileLevel()) {
throw new IllegalStateException("Cannot add a recorder except at the file level");
}
Instance root = this.parent;
assert root != null : "The root element can never be source level";
assert this.measurementFiles != null;
synchronized (this.measurementFiles) {
Map<File, Collection<Object>> fileSet = this.measurementFiles.get(recorder);
if (fileSet == null) {
this.measurementFiles.put(recorder, fileSet = new HashMap<File, Collection<Object>>());
}
Collection<Object> memos = fileSet.get(measurementFile);
if (memos == null) {
fileSet.put(measurementFile, memos = new ArrayList<Object>());
}
memos.add(memo);
}
while (root.parent != null) {
root = root.parent;
}
synchronized (root.measurementFiles) {
if (root.measurementFiles.containsKey(recorder)) {
root.measurementFiles.get(recorder).put(measurementFile, null);
} else {
root.measurementFiles.put(recorder,
new HashMap<File, Collection<Object>>(
Collections.<File, Collection<Object>>singletonMap(measurementFile, null)));
}
}
}
/**
* Returns all the child element types.
*
* @return all the child element types.
*/
public Set<Element> getChildElements() {
return Collections.unmodifiableSet(children.keySet());
}
/**
* Returns all the children of a specific child element type.
*
* @param element The child element type.
*
* @return All the children of the child element type.
*/
public Map<String, Instance> getChildren(Element element) {
if (!this.element.getChildren().contains(element)) {
throw new IllegalArgumentException("A " + element + " is not a child of " + this.element);
}
return Collections.unmodifiableMap(children.get(element));
}
/**
* Returns the measurement of a specific metric.
*
* @param metric The metric.
*
* @return The measurement of the metric.
*/
public Measurement getMeasurement(Metric metric) {
metric.getClass();
return measurements.get(metric);
}
/**
* Returns all the measurements.
*
* @return all the measurements.
*/
public Map<Metric, Measurement> getMeasurements() {
return Collections.unmodifiableMap(measurements);
}
/**
* Returns the available metrics on this instance.
*
* @return the available metrics on this instance.
*/
public Set<Metric> getMetrics() {
return Collections.unmodifiableSet(measurements.keySet());
}
/**
* Creates a new child instance.
*
* @param element The child element type.
* @param name The child name.
*
* @return The child instance.
*/
public Instance newChild(Element element, String name) {
Instance child = new Instance(element, this, name);
addChild(child);
return child;
}
/**
* Looks up an existing child instance or creates a new child instance.
*
* @param element The child element type.
* @param name The child name.
*
* @return The child instance.
*/
public Instance findOrCreateChild(Element element, String name) {
if (!this.element.getChildren().contains(element)) {
throw new IllegalArgumentException("A " + element + " is not a child of " + this.element);
}
final Map<String, Instance> map = children.get(element);
Instance i = map == null ? null : map.get(name);
return (i == null) ? newChild(element, name) : i;
}
/**
* Creates a new child instance corresponding with a file level element.
*
* @param element The child element type.
* @param name The child name.
* @param sourceFile The source file.
*
* @return The child instance.
*/
public Instance newChild(Element element, String name, File sourceFile) {
Instance child = new Instance(element, this, name, sourceFile);
addChild(child);
return child;
}
/**
* Looks up an existing child instance or creates a new child instance corresponding with a file level element.
*
* @param element The child element type.
* @param name The child name.
* @param sourceFile The source file.
*
* @return The child instance.
*/
public Instance findOrCreateChild(Element element, String name, File sourceFile) {
if (!this.element.getChildren().contains(element)) {
throw new IllegalArgumentException("A " + element + " is not a child of " + this.element);
}
final Map<String, Instance> map = children.get(element);
Instance i = map.get(name);
return (i == null) ? newChild(element, name, sourceFile) : i;
}
/**
* Add's a child instance.
*
* @param child The child.
*/
private void addChild(Instance child) {
child.getClass(); // throw NPE if null
if (!element.getChildren().contains(child.element)) {
throw new IllegalArgumentException("A " + child.element + " is not a child of " + element);
}
Map<String, Instance> map = children.get(child.element);
if (map == null) {
children.put(child.element, map = new TreeMap<String, Instance>());
}
map.put(child.name, child);
if (child.sourceFile != null) {
Instance root = this;
while (root.parent != null) {
root = root.parent;
}
synchronized (root.sourceFiles) {
root.sourceFiles.add(sourceFile);
}
}
}
/**
* Set's the measurement of a specific metric
*
* @param metric The metric.
* @param measurement The metric's measurement.
*/
public void setMeasurement(Metric metric, Measurement measurement) {
metric.getClass();
measurement.getClass();
if (!metric.getClazz().isInstance(measurement)) {
throw new IllegalArgumentException(
"Measurements of " + metric.getName() + " must implement " + metric.getClazz());
}
measurements.put(metric, measurement);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Instance i = parent;
while (i != null) {
builder.append(" ");
i = i.parent;
}
builder.append('"');
builder.append(name);
builder.append('"');
builder.append(" [");
builder.append(element.getFullName());
builder.append(']');
builder.append("\n");
for (Map<String, Instance> child : children.values()) {
for (Instance j : child.values()) {
builder.append(j);
}
}
return builder.toString();
}
}