Test coverage report for MetricStore.java - www.sdmetrics.com

/*
 * SDMetrics Open Core for UML design measurement
 * Copyright (c) Juergen Wuest
 * To contact the author, see <http://www.sdmetrics.com/Contact.html>.
 * 
 * This file is part of the SDMetrics Open Core.
 * 
 * SDMetrics Open Core is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.

 * SDMetrics Open Core is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with SDMetrics Open Core.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.sdmetrics.metrics;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.sdmetrics.math.ExpressionNode;
import com.sdmetrics.math.ExpressionParser;
import com.sdmetrics.model.MetaModel;
import com.sdmetrics.model.MetaModelElement;
import com.sdmetrics.util.SAXHandler;

/**
 * Reads a metric definition file and provides access to metrics, sets, and
 * rules defined therein.
 * <p>
 * The class also maintains the matrices, literature references, glossary
 * entries, and word lists defined in the file, as well as custom defined
 * metric, set, and rule procedures, and custom scalar, boolean, or set
 * operations.
 */
public class MetricStore {

	/** The name of the top level XML element in the metric definition file. */
	public static final String TL_ELEMENT = "sdmetrics";

	/** Metamodel on which the metric, set, and rule definitions are based. */
	private final MetaModel metaModel;

	/** The standard and custom defined metric calculation procedures. */
	private final MetricProcedureCache metricProcedures;
	/** The standard and custom defined set calculation procedures. */
	private final SetProcedureCache setProcedures;
	/** The standard and custom defined rule calculation procedures. */
	private final RuleProcedureCache ruleProcedures;
	/** The standard and custom defined expression operations. */
	private final ExpressionOperations exprOps;

	/**
	 * Container for the metrics. Metrics are stored by model element type. For
	 * each type there is one mapping of metric name (key) to the associated
	 * metric.
	 */
	private final HashMap<MetaModelElement, LinkedHashMap<String, Metric>> metrics;

	/** Container for the sets. Same organization as for metrics. */
	private final HashMap<MetaModelElement, LinkedHashMap<String, Set>> sets;

	/** Container for the sets. Same organization as for metrics. */
	private final HashMap<MetaModelElement, LinkedHashMap<String, Rule>> rules;

	/** The relation matrices. */
	private final ArrayList<Matrix> matrices;
	/** The literature references. Key is the reference tag (such as "CK94") */
	private final LinkedHashMap<String, Reference> references;
	/** The glossary. Key is the glossary term. */
	private final LinkedHashMap<String, Glossary> glossary;
	/** The word lists. Key is the name of the list. */
	private final HashMap<String, WordList> wordLists;

	/** The element type with information about rule exemptions. */
	private MetaModelElement ruleExemptionType;
	/**
	 * Name of the attribute or metric that contains the rule exemption
	 * information.
	 */
	private String ruleExemptionTag;

	/** Indicates if metrics/sets/rule should be inheritable by default. */
	private boolean defaultInheritability;

	/**
	 * Creates a new metric store.
	 * 
	 * @param metaModel Metamodel on which the metrics are based.
	 */
	public MetricStore(MetaModel metaModel) {
		this.metaModel = metaModel;

		metricProcedures = new MetricProcedureCache();
		setProcedures = new SetProcedureCache();
		ruleProcedures = new RuleProcedureCache();
		exprOps = new ExpressionOperations();

		metrics = new HashMap<>();
		sets = new HashMap<>();
		rules = new HashMap<>();
		for (MetaModelElement type : metaModel) {
			metrics.put(type, new LinkedHashMap<String, Metric>());
			sets.put(type, new LinkedHashMap<String, Set>());
			rules.put(type, new LinkedHashMap<String, Rule>());
		}

		matrices = new ArrayList<>();
		references = new LinkedHashMap<>();
		glossary = new LinkedHashMap<>();
		wordLists = new HashMap<>();
	}

	/**
	 * Gets a SAX handler to parse a metric definition file and store its
	 * contents with this object.
	 * 
	 * @return SAX handler to parse metrics definition file
	 */
	public DefaultHandler getSAXParserHandler() {
		return new MetricParser();
	}

	/**
	 * Gets the metamodel on which the metrics are based.
	 * 
	 * @return the metamodel for this metric store
	 */
	public MetaModel getMetaModel() {
		return metaModel;
	}

	/**
	 * Gets the cache holding the metric procedure definitions for this metric
	 * store.
	 * 
	 * @return the metric procedure definitions for this store.
	 */
	MetricProcedureCache getMetricProcedures() {
		return metricProcedures;
	}

	/**
	 * Gets the cache holding the set procedure definitions for this metric
	 * store.
	 * 
	 * @return the set procedure definitions for this store.
	 */
	SetProcedureCache getSetProcedures() {
		return setProcedures;
	}

	/**
	 * Gets the cache holding the rule procedure definitions for this metric
	 * store.
	 * 
	 * @return the rule procedure definitions for this store.
	 */
	RuleProcedureCache getRuleProcedures() {
		return ruleProcedures;
	}

	/**
	 * Gets the cache holding the scalar, boolean, and set operations for the
	 * metric expressions in this metric store.
	 * 
	 * @return metric expressions in this store.
	 */
	ExpressionOperations getExpressionOperations() {
		return exprOps;
	}

	/**
	 * Gets the metric for a given element type by its name.
	 * 
	 * @param type Element type for which the metric is defined.
	 * @param name Name of the metric.
	 * @return the specified metric, or <code>null</code> if no such metric was
	 *         found.
	 */
	public Metric getMetric(MetaModelElement type, String name) {
		return metrics.get(type).get(name);
	}

	/**
	 * Gets the set for a given element type by its name.
	 * 
	 * @param type Element type for which the set is defined.
	 * @param name Name of the set.
	 * @return the specified set, or <code>null</code> if no such set was found.
	 */
	public Set getSet(MetaModelElement type, String name) {
		return sets.get(type).get(name);
	}

	/**
	 * Gets the rule for a given element type by its name.
	 * 
	 * @param type Element type for which the rule is defined.
	 * @param name Name of the rule.
	 * @return the specified rule, or <code>null</code> if no such rule was
	 *         found.
	 * @since 2.3
	 */
	public Rule getRule(MetaModelElement type, String name) {
		return rules.get(type).get(name);
	}

	/**
	 * Gets the list of all metrics for a given element type.
	 * 
	 * @param type Element type for which to return the metrics.
	 * @return The metrics for the type. Iteration over this collection
	 *         preserves the order in which the metrics are defined in the
	 *         metric definition file.
	 */
	public Collection<Metric> getMetrics(MetaModelElement type) {
		return metrics.get(type).values();
	}

	/**
	 * Gets the list of all sets for a given element type.
	 * 
	 * @param type Element type for which to return the sets.
	 * @return The sets for the type.
	 */
	public Collection<Set> getSets(MetaModelElement type) {
		return sets.get(type).values();
	}

	/**
	 * Gets the list of all rules for a given element type.
	 * 
	 * @param type Element type for which to return the rules.
	 * @return The rules for the type. Iteration over this collection preserves
	 *         the order in which the rules are defined in the metric definition
	 *         file.
	 */
	public Collection<Rule> getRules(MetaModelElement type) {
		return rules.get(type).values();
	}

	/**
	 * Gets the list of all relationship matrices defined in the metric
	 * definition file.
	 * 
	 * @return Random access list containing the matrices in the order they
	 *         appear in the metric definition file.
	 */
	public List<Matrix> getMatrices() {
		return matrices;
	}

	/**
	 * Gets the list of all literature references defined in the metric
	 * definition file.
	 * 
	 * @return The literature references. Iteration over this collection
	 *         preserves the order in which the references are defined in the
	 *         metric definition file.
	 */
	public Collection<Reference> getLiteratureReferences() {
		return references.values();
	}

	/**
	 * Gets the list of all glossary terms defined in the metric definition
	 * file.
	 * 
	 * @return The glossary terms. Iteration over this collection preserves the
	 *         order in which the glossary terms are defined in the metric
	 *         definition file.
	 */
	public Collection<Glossary> getGlossary() {
		return glossary.values();
	}

	/**
	 * Returns the element type that contains information about rule exemptions.
	 * This type is defined by the "ruleexemption" attribute of the top level
	 * XML element in the metric definition file.
	 * 
	 * @return element type with rule exemption information, or
	 *         <code>null</code> if no such type was specified.
	 */
	MetaModelElement getRuleExemptionType() {
		return ruleExemptionType;
	}

	/**
	 * Returns the name of the attribute or metric that yields the rule
	 * exemption information. This name is defined by the "ruleexemption"
	 * attribute of the top level XML element in the metric definition file.
	 * 
	 * @return Name of the rule exemption attribute or metric, or
	 *         <code>null</code> if none was specified.
	 */
	String getRuleExemptionTag() {
		return ruleExemptionTag;
	}

	/**
	 * Checks if a specified word is on a word list.
	 * 
	 * @param word The word to search for.
	 * @param listName The name of the list to search.
	 * @return <code>true</code> if the specified list contains the word.
	 * @throws SDMetricsException The specified list does not exist.
	 */
	boolean isWordOnList(String word, String listName)
			throws SDMetricsException {
		WordList list = wordLists.get(listName);
		if (list == null) {
			throw new SDMetricsException(null, null, listName
					+ ": No such word list.");
		}
		return list.hasWord(word);
	}

	/** Represents a word list in the metric definition file. */
	static class WordList {
		/** Indicates if string comparisons should be case-sensitive. */
		private final boolean caseSensitive;
		/** The set of words on the list. */
		private final HashSet<String> words;

		/**
		 * Construct a new word list.
		 * 
		 * @param isCaseSensitive Indicates if string comparisons should be case
		 *        sensitive or not.
		 */
		WordList(boolean isCaseSensitive) {
			caseSensitive = isCaseSensitive;
			words = new HashSet<>();
		}

		/**
		 * Adds a word to the word list.
		 * 
		 * @param word The word to add.
		 */
		void addWord(String word) {
			if (caseSensitive) {
				words.add(word);
			} else {
				words.add(word.toUpperCase(Locale.ENGLISH));
			}
		}

		/**
		 * Tests if a word is on the word list.
		 * 
		 * @param word The word to test.
		 * @return <code>true</code> if the word is on the list
		 */
		boolean hasWord(String word) {
			if (caseSensitive) {
				return words.contains(word);
			}
			return words.contains(word.toUpperCase(Locale.ENGLISH));
		}
	}

	/**
	 * SAX handler to parse a metric definition file.
	 */
	private class MetricParser extends SAXHandler {

		// XML element and attribute names in the metric definition file
		private static final String METRICPROCEDURE = "metricprocedure";
		private static final String SETPROCEDURE = "setprocedure";
		private static final String RULEPROCEDURE = "ruleprocedure";
		private static final String SCALAROPERATION = "scalaroperationdefinition";
		private static final String SETOPERATION = "setoperationdefinition";
		private static final String BOOLEANOPERATION = "booleanoperationdefinition";

		private static final String METRICENTRY = "metric";
		private static final String SETENTRY = "set";
		private static final String MATRIXENTRY = "matrix";
		private static final String RULEENTRY = "rule";
		private static final String WORDLIST = "wordlist";
		private static final String WORDLISTENTRY = "entry";
		private static final String REFERENCEENTRY = "reference";
		private static final String GLOSSARYENTRY = "term";
		private static final String DESCRIPTION = "description";
		private static final String INHERITABLE = "inheritable";

		private MetricEntry currentEntry = null;
		private String currentEntryType = null;
		private String currentEntryName = null;
		private WordList currentWordList;
		private boolean inDescription = false;
		private ExpressionParser ep = exprOps.getExpressionParser();

		/**
		 * Process an XML element in the metric definition file.
		 * 
		 * @throws SAXException An error occurred processing an XML element (bad
		 *         definition file).
		 */
		@Override
		public void startElement(String uri, String local, String raw,
				Attributes attrs) throws SAXException {
			if (MetricStore.TL_ELEMENT.equals(raw)) {
				checkVersion(attrs, null);
				String exemptionTypeString = attrs.getValue("ruleexemption");
				if (exemptionTypeString != null) {
					ruleExemptionType = metaModel.getType(exemptionTypeString);
					if (ruleExemptionType == null) {
						throw buildSAXException("Unknown rule exemption type '" 
								+ exemptionTypeString + "'.");
					}
				}
				ruleExemptionTag = attrs.getValue("exemptiontag");
				defaultInheritability = isAttributeValueTrue(attrs, INHERITABLE);
			} else if (DESCRIPTION.equals(raw)) {
				inDescription = true;
			} else if (METRICPROCEDURE.equals(raw)) {
				handleProcedureDefinition(metricProcedures, raw, attrs);
			} else if (SETPROCEDURE.equals(raw)) {
				handleProcedureDefinition(setProcedures, raw, attrs);
			} else if (RULEPROCEDURE.equals(raw)) {
				handleProcedureDefinition(ruleProcedures, raw, attrs);
			} else if (SCALAROPERATION.equals(raw)) {
				String opName = handleProcedureDefinition(
						exprOps.getScalarOperations(), raw, attrs);
				exprOps.addCustomFunctionName(opName);
			} else if (SETOPERATION.equals(raw)) {
				String opName = handleProcedureDefinition(
						exprOps.getSetOperations(), raw, attrs);
				exprOps.addCustomFunctionName(opName);
			} else if (BOOLEANOPERATION.equals(raw)) {
				String opName = handleProcedureDefinition(
						exprOps.getBooleanOperations(), raw, attrs);
				exprOps.addCustomFunctionName(opName);
			} else if (METRICENTRY.equals(raw)) {
				currentEntryType = raw;
				currentEntry = handleMetricDefinition(attrs);
			} else if (SETENTRY.equals(raw)) {
				currentEntryType = raw;
				currentEntry = handleSetDefinition(attrs);
			} else if (MATRIXENTRY.equals(raw)) {
				currentEntryType = raw;
				currentEntry = handleMatrixDefinition(attrs);
			} else if (RULEENTRY.equals(raw)) {
				currentEntryType = raw;
				currentEntry = handleRuleDefinition(attrs);
			} else if (REFERENCEENTRY.equals(raw)) {
				currentEntryType = raw;
				inDescription = true;
				currentEntry = handleReferenceDefinition(attrs);
			} else if (GLOSSARYENTRY.equals(raw)) {
				currentEntryType = raw;
				inDescription = true;
				currentEntry = handleGlossaryDefinition(attrs);
			} else if (WORDLIST.equals(raw)) {
				currentEntryType = raw;
				currentWordList = handleWordListDefinition(attrs);
			} else if (WORDLISTENTRY.equals(raw)) {
				handleWordListEntry(attrs);
			} else {
				handleComputationDefinition(raw, attrs);
			}
		}

		/**
		 * Adds text to the description of the current entry.
		 */
		@Override
		public void characters(char[] ch, int start, int length) {
			if (inDescription && currentEntry != null && length > 0) {
				currentEntry.addDescription(ch, start, length);
			}
		}

		/**
		 * Performs consistency checks at the end of an XML element.
		 */
		@Override
		public void endElement(String uri, String local, String raw)
				throws SAXException {
			if (raw.equals(DESCRIPTION) || raw.equals(GLOSSARYENTRY)
					|| raw.equals(REFERENCEENTRY)) {
				inDescription = false;
				if (!raw.equals(DESCRIPTION)) {
					currentEntry = null;
				}
			} else if (raw.equals(METRICENTRY) || raw.equals(SETENTRY)
					|| raw.equals(RULEENTRY) || raw.equals(MATRIXENTRY)) {
				if (currentEntry.getAttributes() == null) {
					throw buildSAXException("No calculation procedure specified for " + raw
							+ " '" + currentEntry.getName() + "'.");
				}
				currentEntry = null;
			} else if (raw.equals(WORDLIST)) {
				currentWordList = null;
			}
		}

		/**
		 * Calculates metric/set/rule inheritance after all transformations have
		 * been read.
		 */
		@Override
		public void endDocument() {
			HashSet<MetaModelElement> processedTypes = new HashSet<>();
			for (MetaModelElement type : metaModel) {
				processInheritance(type, processedTypes);
			}
		}

		/**
		 * Recursively adds the metrics/sets/rules that the type inherits from
		 * its parent type.
		 * 
		 * @param trans The type to process.
		 * @param processedTransformations The set of types already processed
		 */
		private void processInheritance(MetaModelElement type,
				HashSet<MetaModelElement> processedTypes) {
			// make sure each type is processed only once
			if (processedTypes.contains(type)) {
				return;
			}
			processedTypes.add(type);

			// Recursively process the parent type first
			MetaModelElement parentType = type.getParent();
			if (parentType == null) {
				return;
			}
			processInheritance(parentType, processedTypes);

			// Process metric inheritance for the type
			new InheritanceProcessor<Metric>() {
				@Override
				Metric createCopy(Metric original, MetaModelElement newType) {
					return new Metric(original, newType);
				}
			}.processInheritance(metrics, parentType, type);

			// process set inheritance for the type
			new InheritanceProcessor<Set>() {
				@Override
				Set createCopy(Set original, MetaModelElement newType) {
					return new Set(original, newType);
				}
			}.processInheritance(sets, parentType, type);

			// process rule inheritance for the type
			new InheritanceProcessor<Rule>() {
				@Override
				Rule createCopy(Rule original, MetaModelElement newType) {
					return new Rule(original, newType);
				}
			}.processInheritance(rules, parentType, type);
		}

		/**
		 * Helper class to copy inherited metrics/sets/rules from parent to
		 * child type.
		 * 
		 * @param <T> The type of elements to copy
		 */
		private abstract class InheritanceProcessor<T extends MetricEntry> {
			abstract T createCopy(T original, MetaModelElement newType);

			void processInheritance(
					HashMap<MetaModelElement, LinkedHashMap<String, T>> entries,
					MetaModelElement parentType, MetaModelElement type) {

				Map<String, T> parentEntries = entries.get(parentType);
				Map<String, T> childEntries = entries.get(type);
				for (Map.Entry<String, T> entry : parentEntries.entrySet()) {
					T metricEntry = entry.getValue();
					// if entry is inheritable, and this type does not have an
					// entry under that name, copy the entry for this type
					if (metricEntry.isInheritable()
							&& !childEntries.containsKey(entry.getKey())) {
						T copy = createCopy(metricEntry, type);
						addEntry(childEntries, copy);
					}
				}
			}
		}

		/* Process a metric definition in the metric definition file. */
		private Metric handleMetricDefinition(Attributes attrs)
				throws SAXException {
			String name = getEntryName(attrs);
			MetaModelElement type = getTypeName(attrs, "domain");

			if (getMetric(type, name) != null) {
				throw buildSAXException("Duplicate definition of metric '" + name
						+ "' for elements of type '" + type.getName() + "'.");
			}

			String category = getOptionalAttribute(attrs, "category");

			Metric metric = new Metric(name, type, category);
			metric.setInternal(isAttributeValueTrue(attrs, "internal"));
			setLocation(metric);
			setInheritability(metric, attrs);

			// add the metric to the store
			addEntry(metrics.get(type), metric);
			return metric;
		}

		private String getEntryName(Attributes attrs) throws SAXException {
			currentEntryName = attrs.getValue("name");
			if (currentEntryName == null) {
				throw buildSAXException("No name specified for " + currentEntryType + ".");
			}
			return currentEntryName;
		}

		private MetaModelElement getTypeName(Attributes attrs,
				String domAttribute) throws SAXException {
			String typeName = attrs.getValue(domAttribute);
			if (typeName == null) {
				throw buildSAXException("No domain specified for " + currentEntryType
						+ " '" + currentEntryName + "'.");
			}
			MetaModelElement type = metaModel.getType(typeName);
			if (type == null) {
				throw buildSAXException("Unknown domain '" + typeName + "' for "
						+ currentEntryType + " '" + currentEntryName 
						+ "'.");
			}
			return type;
		}

		private String getOptionalAttribute(Attributes attrs, String attrName) {
			String value = attrs.getValue(attrName);
			if (value == null) {
				return "";
			}
			return value;
		}

		private boolean isAttributeValueTrue(Attributes attrs, String attrName) {
			return "true".equals(attrs.getValue(attrName));
		}

		private void setLocation(MetricEntry entry) {
			if (locator != null) {
				entry.setLocation(locator.getLineNumber());
			}
		}

		private void setInheritability(MetricEntry entry, Attributes attrs) {
			boolean inheritable = defaultInheritability;
			if (attrs.getValue(INHERITABLE) != null) {
				inheritable = isAttributeValueTrue(attrs, INHERITABLE);
			}
			entry.setInheritable(inheritable);
		}

		private <T extends MetricEntry> void addEntry(Map<String, T> map,
				T entry) {
			entry.setId(map.size());
			map.put(entry.getName(), entry);
		}

		/* Process a set definition in the metric definition file. */
		private Set handleSetDefinition(Attributes attrs) throws SAXException {
			String name = getEntryName(attrs);
			MetaModelElement type = getTypeName(attrs, "domain");

			if (getSet(type, name) != null) {
				throw buildSAXException("Duplicate definition of set '" + name
						+ "' for elements of type '" + type.getName() + "'.");
			}

			Set set = new Set(name, type);
			setLocation(set);
			setInheritability(set, attrs);

			set.setMultiSet(isAttributeValueTrue(attrs, "multiset"));

			// assign metric ID to the metric, and add it to the store
			addEntry(sets.get(type), set);
			return set;
		}

		/* Process a relation matrix definition. */
		private Matrix handleMatrixDefinition(Attributes attrs)
				throws SAXException {
			String name = getEntryName(attrs);
			MetaModelElement rowType = getTypeName(attrs, "from_row_type");
			MetaModelElement columnType = getTypeName(attrs, "to_col_type");

			Matrix matrix = new Matrix(name, rowType, columnType);
			setLocation(matrix);

			matrix.setRowCondition(parseExpression(attrs, "row_condition"));
			matrix.setColumnCondition(parseExpression(attrs, "col_condition"));

			// assign ID to the matrix, add it to the store
			matrix.setId(matrices.size());
			matrices.add(matrix);

			return matrix;
		}

		private ExpressionNode parseExpression(Attributes attrs, String attrName)
				throws SAXException {
			ExpressionNode result = null;
			String expr = attrs.getValue(attrName);
			if (expr != null) {
				result = ep.parseExpression(expr);
				if (result == null) {
					throw buildSAXException("Error parsing " + attrName + "='" + expr
							+ "' for " + currentEntryType + " '"
							+ currentEntryName + "':\n" + ep.getErrorInfo());
				}
			}
			return result;
		}

		/* Process a rule definition in the metric definition file. */
		private Rule handleRuleDefinition(Attributes attrs) throws SAXException {
			String name = getEntryName(attrs);

			MetaModelElement type = getTypeName(attrs, "domain");
			if (getRule(type, name) != null) {
				throw buildSAXException("Duplicate definition of rule '" + name
						+ "' for elements of type '" + type.getName() + "'.");
			}

			String category = getOptionalAttribute(attrs, "category");
			String severity = getOptionalAttribute(attrs, "severity");
			boolean enabled = !isAttributeValueTrue(attrs, "disabled");

			ArrayList<String> applicationList = null;
			String appliesTo = attrs.getValue("applies_to");
			if (appliesTo != null) {
				applicationList = new ArrayList<>();
				StringTokenizer st = new StringTokenizer(appliesTo,
						" ,\t\n\r\f");
				while (st.hasMoreTokens()) {
					applicationList.add(st.nextToken());
				}
				if (applicationList.isEmpty()) {
					applicationList = null;
				}
			}

			Rule rule = new Rule(name, type, category, severity,
					applicationList, enabled);
			setLocation(rule);
			setInheritability(rule, attrs);

			// assign ID to the rule, add it to the store
			addEntry(rules.get(type), rule);
			return rule;
		}

		/** Process a literature reference in the metric definition file. */
		private MetricEntry handleReferenceDefinition(Attributes attrs)
				throws SAXException {
			String tag = attrs.getValue("tag");
			if (tag == null) {
				throw buildSAXException("Reference is missing its tag.");
			}
			if (references.containsKey(tag)) {
				throw buildSAXException("Duplicate definition of reference '" + tag + "'.");
			}
			Reference ref = new Reference(tag);
			references.put(tag, ref);
			return ref;
		}

		/* Process a glossary entry in the metric definition file. */
		private Glossary handleGlossaryDefinition(Attributes attrs)
				throws SAXException {
			String name = getEntryName(attrs);
			if (glossary.containsKey(name)) {
				throw buildSAXException("Duplicate definition of glossary term '" + name
						+ "'.");
			}
			Glossary item = new Glossary(name);
			glossary.put(name, item);
			return item;
		}

		/* Process the definition of a word list in the metric definition file. */
		private WordList handleWordListDefinition(Attributes attrs)
				throws SAXException {
			String listName = getEntryName(attrs);
			if (wordLists.containsKey(listName)) {
				throw buildSAXException("Duplicate definition of word list '" + listName
						+ "'.");
			}
			boolean caseSensitive = !isAttributeValueTrue(attrs, "ignorecase");
			WordList list = new WordList(caseSensitive);
			wordLists.put(listName, list);
			return list;
		}

		/* Process an entry in a word list. */
		private void handleWordListEntry(Attributes attrs) throws SAXException {
			if (currentWordList == null) {
				throw buildSAXException("Wordlist entry outside of a word list.");
			}
			String word = attrs.getValue("word");
			if (word == null) {
				throw buildSAXException("Wordlist entry missing 'word' attribute.");
			}
			currentWordList.addWord(word);
		}

		/**
		 * Handles the computation procedure for the current metric, set,
		 * matrix, or rule. Compiles the expressions of the attributes and adds
		 * them to the computation procedure.
		 * 
		 * @param procedureName Name of the computation procedure
		 * @param attrs The attributes for the computation procedure.
		 * @throws SAXException There was an error parsing an attribute of the
		 *         computation procedure.
		 */
		private void handleComputationDefinition(String procedureName,
				Attributes attrs) throws SAXException {
			if (inDescription) {
				throw buildSAXException("Unexpected XML element '" + procedureName
						+ "' in description.");
			}

			if (currentEntry == null) {
				throw buildSAXException("Unexpected XML element '" + procedureName
						+ "' at this point.");
			}

			if (currentEntry.getAttributes() != null) {
				throw buildSAXException("Unexpected XML element '" + procedureName + "' - "
						+ currentEntryType + " '" + currentEntryName
						+ "' already has a calculation procedure.");
			}
			currentEntry.setProcedureName(procedureName);

			ProcedureAttributes attributes = new ProcedureAttributes();
			int attnum = attrs.getLength();
			for (int i = 0; i < attnum; i++) {
				// Compile expression and store the operator tree.
				String attrName = attrs.getQName(i);
				ExpressionNode root = parseExpression(attrs, attrName);
				// check for old "_exp" suffix from versions 1.2 and earlier,
				// remove it from the attribute name to maintain backwards
				// compatibility
				if (attrName.endsWith("_exp")) {
					attrName = attrName.substring(0, attrName.length() - 4);
				}
				attributes.setExpressionNode(attrName, root);
			}
			currentEntry.setAttributes(attributes);
		}

		/**
		 * Handles the definition of a custom defined metric, set, or rule
		 * procedure. Makes sure the procedure class can be loaded and
		 * instantiated.
		 * 
		 * @param store The applicable procedure store
		 * @param element the name of the XML element defining the procedure
		 * @param attrs the XML attributes of the element
		 * @return Name of the procedure
		 * @throws SAXException Missing attributes or procedure class could not
		 *         be loaded
		 */
		private String handleProcedureDefinition(ProcedureCache<?> store,
				String element, Attributes attrs) throws SAXException {
			String procedureName = attrs.getValue("name");
			if (procedureName == null) {
				throw buildSAXException("The " + element
						+ " definition requires a 'name' attribute.");
			}

			if (store.hasProcedure(procedureName)) {
				throw buildSAXException("The " + element + " '" + procedureName
						+ "' is already defined.");
			}

			String className = attrs.getValue("class");
			if (className == null) {
				throw buildSAXException("No class defined for " + element + " definition '"
						+ procedureName + "'.");
			}

			try {
				store.addProcedureClass(procedureName, className);
			} catch (SDMetricsException ex) {
				throw buildSAXException(ex.getMessage());
			}
			return procedureName;
		}
	}
}