Test coverage report for MetaModel.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.model;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

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

import com.sdmetrics.util.SAXHandler;

/**
 * Represents an SDMetrics metamodel. The metamodel defines the available model
 * element types and their attributes.
 * <p>
 * The class parses the metamodel definition file, manages the set of
 * {@link MetaModelElement} instances that constitute the metamodel, and
 * provides access to them.
 */
public class MetaModel implements Iterable<MetaModelElement> {

	/** The name of the top level XML element in the metamodel definition file. */
	public static final String TL_ELEMENT = "sdmetricsmetamodel";
	/** The name of the base element type. */
	public static final String BASE_ELEMENT = "sdmetricsbase";

	/**
	 * Directory of the element types that constitute this metamodel. Allows
	 * lookup of the element types by their name.
	 */
	private final Map<String, MetaModelElement> elementMap = 
			new LinkedHashMap<>();

	/**
	 * Retrieves an iterator over the element types in this metamodel. Types are
	 * returned in the order in which they were added/defined in the metamodel
	 * definition file.
	 * 
	 * @return Metamodel element iterator
	 */
	@Override
	public Iterator<MetaModelElement> iterator() {
		return elementMap.values().iterator();
	}

	/**
	 * Gets a SAX handler to populate this metamodel with the contents of a
	 * metamodel definition file.
	 * 
	 * @return SAX handler to parse the metamodel definition file
	 */
	public DefaultHandler getSAXParserHandler() {
		return new MetaModelParser();
	}

	/**
	 * Retrieves a metamodel element by its type name.
	 * 
	 * @param typeName The type name of the metamodel element.
	 * @return The metamodel element with that name or <code>null</code> if
	 *         there is no element of that name.
	 */
	public MetaModelElement getType(String typeName) {
		return elementMap.get(typeName);
	}

	/**
	 * Gets the number of element types defined by this metamodel.
	 * 
	 * @return The number of types in this metamodel.
	 */
	int getNumberOfTypes() {
		return elementMap.size();
	}

	/**
	 * SAX handler to parse a metamodel definition file.
	 */
	private class MetaModelParser extends SAXHandler {
		// XML element and attribute names of the metamodel definition
		private static final String ELEM_MODELELEMENT = "modelelement";
		private static final String ATTR_NAME = "name";
		private static final String ATTR_PARENT = "parent";
		private static final String ELEM_ATTRIBUTE = "attribute";
		private static final String ATTR_TYPE = "type";
		private static final String ATTR_MULTIPLICITY = "multiplicity";

		private MetaModelElement currentMME = null;
		private String currentAttributeName = null;

		/** Clear the metamodel at the beginning of the XML document. */
		@Override
		public void startDocument() {
			elementMap.clear();
		}

		/**
		 * Process an XML element in the metamodel definition file. Extracts and
		 * stores the metamodel element or attribute information.
		 * 
		 * @throws SAXException The XML file contains something other than model
		 *         element and attribute definitions.
		 */
		@Override
		public void startElement(String uri, String local, String raw,
				Attributes attrs) throws SAXException {
			if (TL_ELEMENT.equals(raw)) {
				checkVersion(attrs, null);
			} else if (ELEM_MODELELEMENT.equals(raw)) {
				handleMetaModelElement(attrs);
			} else if (ELEM_ATTRIBUTE.equals(raw)) {
				handleAttributeDefinition(attrs);
			} else {
				throw buildSAXException("Unexpected XML element <" + raw + ">.");
			}
		}

		/**
		 * Handles a metamodel element definition.
		 * 
		 * @param attrs XML attributes of the element
		 * @throws SAXException metamodel element definition is invalid
		 */
		private void handleMetaModelElement(Attributes attrs)
				throws SAXException {
			String typeName = attrs.getValue(ATTR_NAME);
			if (typeName == null) {
				throw buildSAXException("Model element is missing \"" + ATTR_NAME
						+ "\" attribute.");
			}
			String parentName = attrs.getValue(ATTR_PARENT);
			if (parentName == null) {
				parentName = BASE_ELEMENT;
			}
			MetaModelElement parentElement = elementMap.get(parentName);
			if (parentElement == null && !BASE_ELEMENT.equals(typeName)) {
				if (BASE_ELEMENT.equals(parentName)) {
					throw buildSAXException("The first metamodel element to be defined must be named \""
							+ BASE_ELEMENT + "\".");
				}
				throw buildSAXException("Unknown parent type \"" + parentName
						+ "\" for model element \"" + typeName + "\".");
			}

			currentMME = new MetaModelElement(typeName, parentElement);
			elementMap.put(typeName, currentMME);
			currentAttributeName = null;
		}

		/**
		 * Handles a metamodel attribute definition.
		 * 
		 * @param attrs XML attributes of the metamodel attribute
		 * @throws SAXException metamodel attribute definition is invalid
		 */
		private void handleAttributeDefinition(Attributes attrs)
				throws SAXException {
			if (currentMME == null) {
				throw buildSAXException("Attribute definition outside model element definition.");
			}

			currentAttributeName = attrs.getValue(ATTR_NAME);
			if (currentAttributeName == null) {
				throw buildSAXException("Attribute without a name for model element \""
						+ currentMME.getName() + "\".");
			}

			boolean isRefAttribute = "ref".equals(attrs.getValue(ATTR_TYPE));
			boolean isSetAttribute = "many".equals(attrs
					.getValue(ATTR_MULTIPLICITY));

			if ("extref".equals(attrs.getValue(ATTR_TYPE))) {
				if (currentMME.getExtensionReference() != null) {
					throw buildSAXException("Duplicate extension reference attribute '"
							+ currentAttributeName + "'.");
				}
				if (isSetAttribute) {
					throw buildSAXException("Extension reference attribute '"
							+ currentAttributeName
							+ "' cannot be multi-valued.");
				}
				isRefAttribute = true;
				currentMME.setExtensionReference(currentAttributeName);
			}

			currentMME.addAttribute(currentAttributeName, isRefAttribute,
					isSetAttribute);
		}

		/** Registers end of metamodel element/attribute definitions. */
		@Override
		public void endElement(String uri, String local, String raw) {
			if (ELEM_MODELELEMENT.equals(raw)) {
				currentMME = null;
			} else if (ELEM_ATTRIBUTE.equals(raw)) {
				currentAttributeName = null;
			}
		}

		/**
		 * Adds description text to the current attribute of the current element
		 * type.
		 */
		@Override
		public void characters(char[] ch, int start, int length) {
			if (currentMME != null && currentAttributeName != null) {
				currentMME.addAttributeDescription(currentAttributeName,
						new String(ch, start, length));
			}
		}
	}
}