XmlDataWriter.java

/*******************************************************************************
 * Copyright 2013 André Rouél
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package net.sf.uadetector.writer;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.SortedSet;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import net.sf.qualitycheck.Check;
import net.sf.uadetector.internal.data.BrowserOperatingSystemMappingComparator;
import net.sf.uadetector.internal.data.Data;
import net.sf.uadetector.internal.data.IdentifiableComparator;
import net.sf.uadetector.internal.data.OrderedPatternComparator;
import net.sf.uadetector.internal.data.domain.Browser;
import net.sf.uadetector.internal.data.domain.BrowserOperatingSystemMapping;
import net.sf.uadetector.internal.data.domain.BrowserPattern;
import net.sf.uadetector.internal.data.domain.BrowserType;
import net.sf.uadetector.internal.data.domain.Device;
import net.sf.uadetector.internal.data.domain.DevicePattern;
import net.sf.uadetector.internal.data.domain.OperatingSystem;
import net.sf.uadetector.internal.data.domain.OperatingSystemPattern;
import net.sf.uadetector.internal.data.domain.Robot;
import net.sf.uadetector.internal.util.RegularExpressionConverter;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This utility is intended to transform an instance of {@code Data} into an <i>UAS data</i> conform XML document and
 * allows us to recreate an <code>uas.xml</code>.
 * 
 * @author André Rouél
 */
@ThreadSafe
public final class XmlDataWriter {

	interface Tag {
		String BOT_INFO_URL = "bot_info_url";
		String BROWSER = "browser";
		String BROWSER_ID = "browser_id";
		String BROWSER_INFO_URL = "browser_info_url";
		String BROWSER_OS = "browser_os";
		String BROWSER_REG = "browser_reg";
		String BROWSER_TYPE = "browser_type";
		String BROWSER_TYPES = "browser_types";
		String BROWSERS = "browsers";
		String BROWSERS_OS = "browsers_os";
		String BROWSERS_REG = "browsers_reg";
		String COMPANY = "company";
		String DATA = "data";
		String DESCRIPTION = "description";
		String DEVICE = "device";
		String DEVICE_ID = "device_id";
		String DEVICE_INFO_URL = "device_info_url";
		String DEVICE_REG = "device_reg";
		String DEVICES = "devices";
		String DEVICES_REG = "devices_reg";
		String FAMILY = "family";
		String ICON = "icon";
		String ID = "id";
		String LABEL = "label";
		String NAME = "name";
		String OPERATING_SYSTEM_REG = "operating_system_reg";
		String OPERATING_SYSTEMS = "operating_systems";
		String OPERATING_SYSTEMS_REG = "operating_systems_reg";
		String ORDER = "order";
		String OS = "os";
		String OS_ID = "os_id";
		String OS_INFO_URL = "os_info_url";
		String REGSTRING = "regstring";
		String ROBOT = "robot";
		String ROBOTS = "robots";
		String TYPE = "type";
		String UASDATA = "uasdata";
		String URL = "url";
		String URL_COMPANY = "url_company";
		String USERAGENT = "useragent";
	}

	private static final String INDENT_AMOUNT = "4";

	private static final String INDENT_OPTION = "yes";

	private static final String SCHEMA_URL = "http://user-agent-string.info/rpc/uasxmldata.dtd";

	private static Element createBrowser(final Browser browser, final Document doc) {
		final Element b = doc.createElement(Tag.BROWSER);
		final Element id = doc.createElement(Tag.ID);
		id.appendChild(doc.createTextNode(String.valueOf(browser.getId())));
		b.appendChild(id);
		final Element family = doc.createElement(Tag.TYPE);
		family.appendChild(doc.createTextNode(String.valueOf(browser.getType().getId())));
		b.appendChild(family);
		final Element name = doc.createElement(Tag.NAME);
		name.appendChild(doc.createTextNode(browser.getFamilyName()));
		b.appendChild(name);
		final Element url = doc.createElement(Tag.URL);
		url.appendChild(doc.createCDATASection(browser.getUrl()));
		b.appendChild(url);
		final Element company = doc.createElement(Tag.COMPANY);
		company.appendChild(doc.createCDATASection(browser.getProducer()));
		b.appendChild(company);
		final Element companyUrl = doc.createElement(Tag.URL_COMPANY);
		companyUrl.appendChild(doc.createCDATASection(browser.getProducerUrl()));
		b.appendChild(companyUrl);
		final Element icon = doc.createElement(Tag.ICON);
		icon.appendChild(doc.createTextNode(browser.getIcon()));
		b.appendChild(icon);
		final Element botInfoUrl = doc.createElement(Tag.BROWSER_INFO_URL);
		botInfoUrl.appendChild(doc.createTextNode(browser.getInfoUrl()));
		b.appendChild(botInfoUrl);
		return b;
	}

	private static Element createBrowserOperatingSystemMappings(final Data data, final Document doc) {
		final List<BrowserOperatingSystemMapping> mappings = new ArrayList<BrowserOperatingSystemMapping>(
				data.getBrowserToOperatingSystemMappings());
		Collections.sort(mappings, BrowserOperatingSystemMappingComparator.INSTANCE);

		final Element browserTypesElement = doc.createElement(Tag.BROWSERS_OS);
		for (final BrowserOperatingSystemMapping mapping : mappings) {
			final Element t = doc.createElement(Tag.BROWSER_OS);
			final Element browserId = doc.createElement(Tag.BROWSER_ID);
			browserId.appendChild(doc.createTextNode(String.valueOf(mapping.getBrowserId())));
			t.appendChild(browserId);
			final Element osId = doc.createElement(Tag.OS_ID);
			osId.appendChild(doc.createTextNode(String.valueOf(mapping.getOperatingSystemId())));
			t.appendChild(osId);
			browserTypesElement.appendChild(t);
		}
		return browserTypesElement;
	}

	private static Element createBrowserPatterns(final Data data, final Document doc) {
		final List<BrowserPattern> patterns = new ArrayList<BrowserPattern>(data.getBrowserPatterns().size());
		for (final Entry<Integer, SortedSet<BrowserPattern>> entry : data.getBrowserPatterns().entrySet()) {
			patterns.addAll(entry.getValue());
		}
		Collections.sort(patterns, new OrderedPatternComparator<BrowserPattern>());

		final Element browserTypesElement = doc.createElement(Tag.BROWSERS_REG);
		for (final BrowserPattern pattern : patterns) {
			final Element t = doc.createElement(Tag.BROWSER_REG);
			final Element order = doc.createElement(Tag.ORDER);
			order.appendChild(doc.createTextNode(String.valueOf(pattern.getPosition())));
			t.appendChild(order);
			final Element id = doc.createElement(Tag.BROWSER_ID);
			id.appendChild(doc.createTextNode(String.valueOf(pattern.getId())));
			t.appendChild(id);
			final Element family = doc.createElement(Tag.REGSTRING);
			family.appendChild(doc.createTextNode(RegularExpressionConverter.convertPatternToPerlRegex(pattern.getPattern())));
			t.appendChild(family);
			browserTypesElement.appendChild(t);
		}
		return browserTypesElement;
	}

	private static Element createBrowsers(final Data data, final Document doc) {
		final Element browsersElement = doc.createElement(Tag.BROWSERS);
		final List<Browser> browsers = new ArrayList<Browser>(data.getBrowsers());
		Collections.sort(browsers, IdentifiableComparator.INSTANCE);
		for (final Browser browser : browsers) {
			browsersElement.appendChild(createBrowser(browser, doc));
		}
		return browsersElement;
	}

	private static Element createBrowserTypes(final Data data, final Document doc) {
		final Element browserTypesElement = doc.createElement(Tag.BROWSER_TYPES);
		final List<BrowserType> browserTypes = new ArrayList<BrowserType>(data.getBrowserTypes().values());
		Collections.sort(browserTypes, IdentifiableComparator.INSTANCE);
		for (final BrowserType browserType : browserTypes) {
			final Element t = doc.createElement(Tag.BROWSER_TYPE);
			final Element id = doc.createElement(Tag.ID);
			id.appendChild(doc.createTextNode(String.valueOf(browserType.getId())));
			t.appendChild(id);
			final Element family = doc.createElement(Tag.TYPE);
			family.appendChild(doc.createTextNode(String.valueOf(browserType.getName())));
			t.appendChild(family);
			browserTypesElement.appendChild(t);
		}
		return browserTypesElement;
	}

	private static Element createDescription(@Nonnull final Data data, @Nonnull final Document doc) {
		final Element description = doc.createElement(Tag.DESCRIPTION);
		final Element label = doc.createElement(Tag.LABEL);
		description.appendChild(label).appendChild(
				doc.createTextNode("Data (format xml) for UASparser - http://user-agent-string.info/download/UASparser"));
		final Element version = doc.createElement("version");
		description.appendChild(version).appendChild(doc.createTextNode(data.getVersion()));
		final Element md5Checksum = doc.createElement("checksum");
		md5Checksum.setAttribute(Tag.TYPE, "MD5");
		description.appendChild(md5Checksum).appendChild(
				doc.createTextNode("http://user-agent-string.info/rpc/get_data.php?format=xml&md5=y"));
		final Element shaChecksum = doc.createElement("checksum");
		shaChecksum.setAttribute(Tag.TYPE, "SHA1");
		description.appendChild(shaChecksum).appendChild(
				doc.createTextNode("http://user-agent-string.info/rpc/get_data.php?format=xml&sha1=y"));
		return description;
	}

	private static Element createDevice(final Device device, final Document doc) {
		final Element b = doc.createElement(Tag.DEVICE);
		final Element id = doc.createElement(Tag.ID);
		id.appendChild(doc.createTextNode(String.valueOf(device.getId())));
		b.appendChild(id);
		final Element name = doc.createElement(Tag.NAME);
		name.appendChild(doc.createTextNode(device.getName()));
		b.appendChild(name);
		final Element icon = doc.createElement(Tag.ICON);
		icon.appendChild(doc.createTextNode(device.getIcon()));
		b.appendChild(icon);
		final Element botInfoUrl = doc.createElement(Tag.DEVICE_INFO_URL);
		botInfoUrl.appendChild(doc.createTextNode(device.getInfoUrl()));
		b.appendChild(botInfoUrl);
		return b;
	}

	private static Element createDevicePatterns(final Data data, final Document doc) {
		final List<DevicePattern> patterns = new ArrayList<DevicePattern>(data.getDevicePatterns().size());
		for (final Entry<Integer, SortedSet<DevicePattern>> entry : data.getDevicePatterns().entrySet()) {
			patterns.addAll(entry.getValue());
		}
		Collections.sort(patterns, new OrderedPatternComparator<DevicePattern>());

		final Element deviceTypesElement = doc.createElement(Tag.DEVICES_REG);
		for (final DevicePattern pattern : patterns) {
			final Element t = doc.createElement(Tag.DEVICE_REG);
			final Element order = doc.createElement(Tag.ORDER);
			order.appendChild(doc.createTextNode(String.valueOf(pattern.getPosition())));
			t.appendChild(order);
			final Element id = doc.createElement(Tag.DEVICE_ID);
			id.appendChild(doc.createTextNode(String.valueOf(pattern.getId())));
			t.appendChild(id);
			final Element family = doc.createElement(Tag.REGSTRING);
			family.appendChild(doc.createTextNode(RegularExpressionConverter.convertPatternToPerlRegex(pattern.getPattern())));
			t.appendChild(family);
			deviceTypesElement.appendChild(t);
		}
		return deviceTypesElement;
	}

	private static Element createDevices(final Data data, final Document doc) {
		final Element devicesElement = doc.createElement(Tag.DEVICES);
		final List<Device> devices = new ArrayList<Device>(data.getDevices());
		Collections.sort(devices, IdentifiableComparator.INSTANCE);
		for (final Device device : devices) {
			devicesElement.appendChild(createDevice(device, doc));
		}
		return devicesElement;
	}

	private static Element createOperatingSystem(final OperatingSystem operatingSystem, final Document doc) {
		final Element os = doc.createElement(Tag.OS);
		final Element id = doc.createElement("id");
		id.appendChild(doc.createTextNode(String.valueOf(operatingSystem.getId())));
		os.appendChild(id);
		final Element family = doc.createElement(Tag.FAMILY);
		family.appendChild(doc.createTextNode(operatingSystem.getFamily()));
		os.appendChild(family);
		final Element name = doc.createElement(Tag.NAME);
		name.appendChild(doc.createTextNode(operatingSystem.getName()));
		os.appendChild(name);
		final Element url = doc.createElement(Tag.URL);
		url.appendChild(doc.createCDATASection(operatingSystem.getUrl()));
		os.appendChild(url);
		final Element company = doc.createElement(Tag.COMPANY);
		company.appendChild(doc.createCDATASection(operatingSystem.getProducer()));
		os.appendChild(company);
		final Element companyUrl = doc.createElement(Tag.URL_COMPANY);
		companyUrl.appendChild(doc.createCDATASection(operatingSystem.getProducerUrl()));
		os.appendChild(companyUrl);
		final Element icon = doc.createElement(Tag.ICON);
		icon.appendChild(doc.createTextNode(operatingSystem.getIcon()));
		os.appendChild(icon);
		final Element botInfoUrl = doc.createElement(Tag.OS_INFO_URL);
		botInfoUrl.appendChild(doc.createTextNode(operatingSystem.getInfoUrl()));
		os.appendChild(botInfoUrl);
		return os;
	}

	private static Element createOperatingSystemPatterns(final Data data, final Document doc) {
		final List<OperatingSystemPattern> patterns = new ArrayList<OperatingSystemPattern>(data.getOperatingSystemPatterns().size());
		for (final Entry<Integer, SortedSet<OperatingSystemPattern>> entry : data.getOperatingSystemPatterns().entrySet()) {
			patterns.addAll(entry.getValue());
		}
		Collections.sort(patterns, new OrderedPatternComparator<OperatingSystemPattern>());

		final Element browserTypesElement = doc.createElement(Tag.OPERATING_SYSTEMS_REG);
		for (final OperatingSystemPattern pattern : patterns) {
			final Element t = doc.createElement(Tag.OPERATING_SYSTEM_REG);
			final Element order = doc.createElement(Tag.ORDER);
			order.appendChild(doc.createTextNode(String.valueOf(pattern.getPosition())));
			t.appendChild(order);
			final Element id = doc.createElement(Tag.OS_ID);
			id.appendChild(doc.createTextNode(String.valueOf(pattern.getId())));
			t.appendChild(id);
			final Element family = doc.createElement(Tag.REGSTRING);
			family.appendChild(doc.createTextNode(RegularExpressionConverter.convertPatternToPerlRegex(pattern.getPattern())));
			t.appendChild(family);
			browserTypesElement.appendChild(t);
		}
		return browserTypesElement;
	}

	private static Element createOperatingSystems(final Data data, final Document doc) {
		final Element operatingSystemsElement = doc.createElement(Tag.OPERATING_SYSTEMS);
		final List<OperatingSystem> operatingSystems = new ArrayList<OperatingSystem>(data.getOperatingSystems());
		Collections.sort(operatingSystems, IdentifiableComparator.INSTANCE);
		for (final OperatingSystem operatingSystem : operatingSystems) {
			operatingSystemsElement.appendChild(createOperatingSystem(operatingSystem, doc));
		}
		return operatingSystemsElement;
	}

	private static Element createRobots(final Data data, final Document doc) {
		final Element robotsElement = doc.createElement(Tag.ROBOTS);
		for (final Robot robot : data.getRobots()) {
			robotsElement.appendChild(createRobots(robot, doc));
		}
		return robotsElement;
	}

	private static Element createRobots(final Robot robot, final Document doc) {
		final Element r = doc.createElement(Tag.ROBOT);
		final Element id = doc.createElement(Tag.ID);
		id.appendChild(doc.createTextNode(String.valueOf(robot.getId())));
		r.appendChild(id);
		final Element useragent = doc.createElement(Tag.USERAGENT);
		useragent.appendChild(doc.createCDATASection(robot.getUserAgentString()));
		r.appendChild(useragent);
		final Element family = doc.createElement(Tag.FAMILY);
		family.appendChild(doc.createTextNode(robot.getFamilyName()));
		r.appendChild(family);
		final Element name = doc.createElement(Tag.NAME);
		name.appendChild(doc.createTextNode(robot.getName()));
		r.appendChild(name);
		final Element company = doc.createElement(Tag.COMPANY);
		company.appendChild(doc.createCDATASection(robot.getProducer()));
		r.appendChild(company);
		final Element companyUrl = doc.createElement(Tag.URL_COMPANY);
		companyUrl.appendChild(doc.createCDATASection(robot.getProducerUrl()));
		r.appendChild(companyUrl);
		final Element icon = doc.createElement(Tag.ICON);
		icon.appendChild(doc.createTextNode(robot.getIcon()));
		r.appendChild(icon);
		final Element botInfoUrl = doc.createElement(Tag.BOT_INFO_URL);
		botInfoUrl.appendChild(doc.createTextNode(robot.getInfoUrl()));
		r.appendChild(botInfoUrl);
		return r;
	}

	@Nonnull
	static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
		final DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
		return docFactory.newDocumentBuilder();
	}

	static void transform(@Nonnull final Source xmlInput, @Nonnull final Result xmlOutput) throws TransformerException {
		Check.notNull(xmlInput, "xmlInput");
		Check.notNull(xmlOutput, "xmlOutput");

		final TransformerFactory transformerFactory = TransformerFactory.newInstance();
		final Transformer transformer = transformerFactory.newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, INDENT_OPTION);
		transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, SCHEMA_URL);
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", INDENT_AMOUNT);
		transformer.transform(xmlInput, xmlOutput);
	}

	/**
	 * Transforms a given {@code Data} instance into XML and writes it to the passed in {@code OutputStream}.
	 * 
	 * @param data
	 *            {@code Data} to transform into XML
	 * @param outputStream
	 *            output stream to write
	 * @throws ParserConfigurationException
	 *             If a DocumentBuilder cannot be created which satisfies the configuration requested.
	 * @throws TransformerException
	 *             If an unrecoverable error occurs during the course of the transformation.
	 */
	public static void write(@Nonnull final Data data, @Nonnull final OutputStream outputStream) throws ParserConfigurationException,
			TransformerException {
		Check.notNull(data, "data");
		Check.notNull(outputStream, "outputStream");

		final Document doc = newDocumentBuilder().newDocument();

		// root element
		final Element uasdataElement = doc.createElement(Tag.UASDATA);
		doc.appendChild(uasdataElement);

		// description element
		uasdataElement.appendChild(createDescription(data, doc));

		// data element
		final Element dataElement = doc.createElement(Tag.DATA);
		uasdataElement.appendChild(dataElement);

		dataElement.appendChild(createRobots(data, doc));
		dataElement.appendChild(createOperatingSystems(data, doc));
		dataElement.appendChild(createBrowsers(data, doc));
		dataElement.appendChild(createBrowserTypes(data, doc));
		dataElement.appendChild(createBrowserPatterns(data, doc));
		dataElement.appendChild(createBrowserOperatingSystemMappings(data, doc));
		dataElement.appendChild(createOperatingSystemPatterns(data, doc));
		dataElement.appendChild(createDevices(data, doc));
		dataElement.appendChild(createDevicePatterns(data, doc));

		// write the content to output stream
		final DOMSource source = new DOMSource(doc);
		final StreamResult result = new StreamResult(outputStream);
		transform(source, result);
	}

	/**
	 * <strong>Attention:</strong> This class is not intended to create objects from it.
	 */
	private XmlDataWriter() {
		// This class is not intended to create objects from it.
	}

}