package hudson.plugins.coverage.model; import java.util.Collections; import java.util.LinkedList; import java.util.Set; import java.util.TreeSet; /** * Represents an element in the source code. * * @author Stephen Connolly * @since 26-Jun-2008 17:04:11 */ public final class Element implements Comparable<Element> { // ------------------------------ FIELDS ------------------------------ /** * The parent source code element. */ private final Element parent; /** * The name of this element. */ private final String name; /** * {@code true} if the element has a one-to-one correspondance with source code files. */ private final boolean fileLevel; /** * The child elements. */ private final transient Set<Element> children = new TreeSet<Element>(); private final transient Model model; /** * Lazily calculated full name of the element. */ private transient String fullName = null; /** * Lazily calculated flag to indicate that this element is below the file level. */ private transient Boolean subfileLevel = null; // -------------------------- STATIC METHODS -------------------------- /** * Gets the root element from which all elements must inherit. * * @return the root element from which all elements must inherit. */ public static Element getRootElement() { return SingletonHolder.ROOT; } /** * Gets the root element from which all elements must inherit. * * @param fullName The full name of the element. * * @return the root element from which all elements must inherit. */ public static Element getElement(String fullName) { return SingletonHolder.ROOT.find(fullName); } /** * Finds an element from its full name. * * @param fullName The full name. * * @return the element of {@code null} if the element does not exist. */ public Element find(String fullName) { if (getFullName().equals(fullName)) { return this; } for (Element element : children) { if (fullName.startsWith(element.getFullName())) { return element.find(fullName); } } return null; } /** * Creates a new source code element. * * @param parent The parent. * @param name The name of the element. * @param fileLevel {@code true} if this element corresponds to a source file. * @param model The model that describes how this element calculates results from its children. * * @return The new element. */ public static Element newElement(Element parent, String name, boolean fileLevel, Model model) { Element result = new Element(parent, name, fileLevel, model); parent.addChild(result); return result; } /** * Adds a child to this, it's parent. * * @param child the child. */ private synchronized void addChild(Element child) { if (child.getParent() != this) { throw new IllegalArgumentException("Cannot add the child of a different parent"); } children.add(child); } // --------------------------- CONSTRUCTORS --------------------------- /** * Constructor for root element. */ private Element() { name = ""; parent = null; fileLevel = false; model = StandardModel.getInstance(); } /** * Constructor for a child element. * * @param parent The parent. * @param name The name. * @param fileLevel {@code true} if this is a file level element. * @param model The model that describes how this element calculates results from its children. */ private Element(Element parent, String name, boolean fileLevel, Model model) { parent.getClass(); // throw NPE if null name.getClass(); // throw NPE if null model.getClass(); // throw NPE if null if (name.indexOf('/') != -1) { throw new IllegalArgumentException("The name of an element caonnot contain the '/' character"); } this.name = name; this.parent = parent; this.fileLevel = fileLevel; this.model = model; } // --------------------- GETTER / SETTER METHODS --------------------- /** * Gets the full name for the element. * * @return the full name for the element. */ public String getFullName() { // don't need to worry about multiple threads as the result must be the same. if (fullName == null) { LinkedList<String> names = new LinkedList<String>(); for (Element i = this; i.getParent() != null; i = i.getParent()) { names.addFirst(i.getName()); } StringBuilder buf = new StringBuilder(); boolean first = true; for (String name : names) { buf.append('/'); buf.append(name); } fullName = buf.toString(); } return fullName; } /** * Getter for property 'model'. * * @return Value for property 'model'. */ public Model getModel() { return model; } /** * Getter for property 'name'. * * @return Value for property 'name'. */ public String getName() { return name; } /** * Getter for property 'parent'. * * @return Value for property 'parent'. */ public Element getParent() { return parent; } /** * Getter for property 'fileLevel'. * * @return Value for property 'fileLevel'. */ public boolean isFileLevel() { return fileLevel; } // ------------------------ CANONICAL METHODS ------------------------ /** * {@inheritDoc} */ public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Element element = (Element) o; if (!name.equals(element.name)) { return false; } if (parent != null ? !parent.equals(element.parent) : element.parent != null) { return false; } return true; } /** * {@inheritDoc} */ public int hashCode() { int result; result = (parent != null ? parent.hashCode() : 0); result = 31 * result + name.hashCode(); return result; } // ------------------------ INTERFACE METHODS ------------------------ // --------------------- Interface Comparable --------------------- /** * {@inheritDoc} */ public int compareTo(Element that) { if (parent == that.parent || (parent != null && parent.equals(that.parent))) { // same parent, so compare based on name return name.compareTo(that.name); } if (parent != null && that.parent != null) { // has two parents and both are not the root, so compare parents return parent.compareTo(that.parent); } if (parent == null) { // this is nearest the root, so comes first assert that.parent != null; return -1; } // that is nearest the root, so comes first. return +1; } // -------------------------- OTHER METHODS -------------------------- /** * Destroys this element. Needed for unit tests. */ synchronized void destroy() { // first destroy all children for (Element child : new TreeSet<Element>(children)) { child.destroy(); } // next unhook ourselves from the parent if (parent != null) { parent.removeChild(this); } } /** * Removes a child element. * * @param element The child. */ private synchronized void removeChild(Element element) { children.remove(element); } /** * Getter for property 'children'. * * @return Value for property 'children'. */ public synchronized Set<Element> getChildren() { return Collections.unmodifiableSet(children); } /** * Getter for property 'subfileLevel'. * * @return Value for property 'subfileLevel'. */ public boolean isSubfileLevel() { // don't need to worry about multiple threads as the result must be the same. if (subfileLevel == null) { if (fileLevel || this.getParent() == null) { subfileLevel = Boolean.FALSE; } else { subfileLevel = Boolean.valueOf(getParent().isSubfileLevel()); } } return subfileLevel.booleanValue(); } /** * Creates a new source code element as a child of {@code this}. * * @param name The name of the element. * @param fileLevel {@code true} if this element corresponds to a source file. * @param model The model that describes how this element calculates results from its children. * * @return The new element. */ public Element newChild(String name, boolean fileLevel, Model model) { Element result = new Element(this, name, fileLevel, model); addChild(result); return result; } @Override public String toString() { return getFullName(); } // -------------------------- INNER CLASSES -------------------------- /** * Holds the root element and ensures that the Element class has fully loaded before constructing it. */ private static final class SingletonHolder { // ------------------------------ FIELDS ------------------------------ /** * The root element singleton. */ private static final Element ROOT = new Element(); // --------------------------- CONSTRUCTORS --------------------------- /** * Do not instantiate SingletonHolder. */ private SingletonHolder() { } } }