DataBuilder.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.internal.data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;

import net.sf.qualitycheck.Check;
import net.sf.qualitycheck.exception.IllegalStateOfArgumentException;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is intended to create instances of {@code Data}.
 * 
 * @author André Rouél
 */
@NotThreadSafe
public class DataBuilder {

	private static final Logger LOG = LoggerFactory.getLogger(DataBuilder.class);

	private static void addOperatingSystemToBrowser(final Map<Integer, Browser.Builder> browserBuilders,
			final Map<Integer, OperatingSystem> operatingSystems, final Map<Integer, Integer> browserOsMap) {
		Browser.Builder browserBuilder;
		for (final Map.Entry<Integer, Integer> entry : browserOsMap.entrySet()) {
			if (browserBuilders.containsKey(entry.getKey())) {
				browserBuilder = browserBuilders.get(entry.getKey());
				if (operatingSystems.containsKey(entry.getValue())) {
					browserBuilder.setOperatingSystem(operatingSystems.get(entry.getValue()));
				} else {
					LOG.warn("Can not find an operating system with ID '" + entry.getValue() + "' for browser '"
							+ browserBuilder.getProducer() + " " + browserBuilder.getFamily() + "'.");
				}
			} else {
				LOG.warn("Can not find a browser with ID '" + entry.getKey() + "'.");
			}
		}
	}

	private static void addPatternToBrowser(final Map<Integer, Browser.Builder> builders,
			final Map<Integer, SortedSet<BrowserPattern>> patterns) {
		for (final Map.Entry<Integer, Browser.Builder> entry : builders.entrySet()) {
			if (patterns.containsKey(entry.getKey())) {
				entry.getValue().setPatterns(patterns.get(entry.getKey()));
			} else {
				LOG.warn("No pattern available for '" + entry.getValue().getProducer() + " " + entry.getValue().getFamily() + "'.");
			}
		}
	}

	private static void addPatternToDevice(final Map<Integer, Device.Builder> builders,
			final Map<Integer, SortedSet<DevicePattern>> patterns) {
		for (final Map.Entry<Integer, Device.Builder> entry : builders.entrySet()) {
			if (patterns.containsKey(entry.getKey())) {
				entry.getValue().setPatterns(patterns.get(entry.getKey()));
			} else {
				LOG.debug("No pattern available for '" + entry.getValue().getName() + "'.");
			}
		}
	}

	private static void addPatternToOperatingSystem(final Map<Integer, OperatingSystem.Builder> builders,
			final Map<Integer, SortedSet<OperatingSystemPattern>> patterns) {
		for (final Map.Entry<Integer, OperatingSystem.Builder> entry : builders.entrySet()) {
			final SortedSet<OperatingSystemPattern> patternSet = patterns.get(entry.getKey());
			if (patternSet != null) {
				entry.getValue().addPatterns(patternSet);
			} else {
				LOG.debug("No patterns for operating system entry (with id '" + entry.getKey() + "') available.");
			}
		}
	}

	private static void addTypeToBrowser(final Map<Integer, Browser.Builder> builders, final Map<Integer, BrowserType> types) {
		int typeId;
		for (final Map.Entry<Integer, Browser.Builder> entry : builders.entrySet()) {
			typeId = entry.getValue().getTypeId();
			if (types.containsKey(typeId)) {
				entry.getValue().setType(types.get(typeId));
			} else {
				LOG.warn("No type available for '" + entry.getValue().getProducer() + " " + entry.getValue().getFamily() + "'.");
			}
		}
	}

	private static Set<Browser> buildBrowsers(final Map<Integer, Browser.Builder> browserBuilders) {
		final Set<Browser> browsers = new HashSet<Browser>();
		for (final Map.Entry<Integer, Browser.Builder> entry : browserBuilders.entrySet()) {
			try {
				browsers.add(entry.getValue().build());
			} catch (final Exception e) {
				LOG.warn("Can not build browser: " + e.getLocalizedMessage());
			}
		}
		return browsers;
	}

	private static Set<Device> buildDevices(final Map<Integer, Device.Builder> deviceBuilders) {
		final Set<Device> devices = new HashSet<Device>();
		for (final Map.Entry<Integer, Device.Builder> entry : deviceBuilders.entrySet()) {
			try {
				devices.add(entry.getValue().build());
			} catch (final Exception e) {
				LOG.warn("Can not build device '" + entry.getValue().getName() + "': " + e.getLocalizedMessage());
			}
		}
		return devices;
	}

	private static Map<Integer, OperatingSystem> buildOperatingSystems(final Map<Integer, OperatingSystem.Builder> osBuilders) {
		final Map<Integer, OperatingSystem> operatingSystems = new HashMap<Integer, OperatingSystem>();
		for (final Map.Entry<Integer, OperatingSystem.Builder> entry : osBuilders.entrySet()) {
			try {
				operatingSystems.put(entry.getKey(), entry.getValue().build());
			} catch (final Exception e) {
				LOG.warn("Can not build operating system: " + e.getLocalizedMessage());
			}
		}
		return operatingSystems;
	}

	private static SortedMap<BrowserPattern, Browser> buildPatternToBrowserMap(final Set<Browser> browserSet) {
		final SortedMap<BrowserPattern, Browser> patternBrowser = new TreeMap<BrowserPattern, Browser>(BROWSER_PATTERN_COMPARATOR);
		for (final Browser browser : browserSet) {
			for (final BrowserPattern pattern : browser.getPatterns()) {
				patternBrowser.put(pattern, browser);
			}
		}
		return patternBrowser;
	}

	private static SortedMap<DevicePattern, Device> buildPatternToDeviceMap(final Set<Device> devices) {
		final SortedMap<DevicePattern, Device> patternDevice = new TreeMap<DevicePattern, Device>(DEVICE_PATTERN_COMPARATOR);
		for (final Device device : devices) {
			for (final DevicePattern pattern : device.getPatterns()) {
				patternDevice.put(pattern, device);
			}
		}
		return patternDevice;
	}

	private static SortedMap<OperatingSystemPattern, OperatingSystem> buildPatternToOperatingSystemMap(final Set<OperatingSystem> osSet) {
		final SortedMap<OperatingSystemPattern, OperatingSystem> map = new TreeMap<OperatingSystemPattern, OperatingSystem>(
				OS_PATTERN_COMPARATOR);
		for (final OperatingSystem os : osSet) {
			for (final OperatingSystemPattern pattern : os.getPatterns()) {
				map.put(pattern, os);
			}
		}
		return map;
	}

	private static Map<Integer, Integer> convertBrowserOsMapping(final Set<BrowserOperatingSystemMapping> browserOperatingSystemMappings) {
		final Map<Integer, Integer> result = new HashMap<Integer, Integer>();
		for (final BrowserOperatingSystemMapping mapping : browserOperatingSystemMappings) {
			result.put(mapping.getBrowserId(), mapping.getOperatingSystemId());
		}
		return result;
	}

	private static Set<OperatingSystem> convertOperatingSystems(final Map<Integer, OperatingSystem> operatingSystems) {
		final Set<OperatingSystem> result = new HashSet<OperatingSystem>();
		for (final Entry<Integer, OperatingSystem> entry : operatingSystems.entrySet()) {
			result.add(entry.getValue());
		}
		return result;
	}

	@Nonnull
	private final Map<Integer, BrowserType> browserTypes = new HashMap<Integer, BrowserType>();

	@Nonnull
	private final Map<Integer, SortedSet<BrowserPattern>> browserPatterns = new HashMap<Integer, SortedSet<BrowserPattern>>();

	@Nonnull
	private final Map<Integer, SortedSet<OperatingSystemPattern>> operatingSystemPatterns = new HashMap<Integer, SortedSet<OperatingSystemPattern>>();

	@Nonnull
	private final Map<Integer, Browser.Builder> browserBuilders = new HashMap<Integer, Browser.Builder>();

	@Nonnull
	private final Set<Browser> browsers = new HashSet<Browser>();

	@Nonnull
	private final Set<Device> devices = new HashSet<Device>();

	@Nonnull
	private final Map<Integer, Device.Builder> deviceBuilders = new HashMap<Integer, Device.Builder>();

	@Nonnull
	private final Map<Integer, SortedSet<DevicePattern>> devicePatterns = new HashMap<Integer, SortedSet<DevicePattern>>();

	@Nonnull
	private final Map<Integer, OperatingSystem.Builder> operatingSystemBuilders = new HashMap<Integer, OperatingSystem.Builder>();

	@Nonnull
	private final Set<OperatingSystem> operatingSystems = new HashSet<OperatingSystem>();

	@Nonnull
	private final List<Robot> robots = new ArrayList<Robot>();

	private String version;

	@Nonnull
	private final Set<BrowserOperatingSystemMapping> browserToOperatingSystemMap = new HashSet<BrowserOperatingSystemMapping>();

	private static final OrderedPatternComparator<BrowserPattern> BROWSER_PATTERN_COMPARATOR = new OrderedPatternComparator<BrowserPattern>();

	private static final OrderedPatternComparator<DevicePattern> DEVICE_PATTERN_COMPARATOR = new OrderedPatternComparator<DevicePattern>();

	private static final OrderedPatternComparator<OperatingSystemPattern> OS_PATTERN_COMPARATOR = new OrderedPatternComparator<OperatingSystemPattern>();

	public DataBuilder appendBrowser(@Nonnull final Browser browser) {
		Check.notNull(browser, "browser");

		browsers.add(browser);
		return this;
	}

	/**
	 * Appends a copy of the given {@code Browser.Builder} to the internal data structure.
	 * 
	 * @param browserBuilder
	 *            {@code Browser.Builder} to be copied and appended
	 * @return this {@code Builder}, for chaining
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the given argument is {@code null}
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if the ID of the given builder is invalid
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if a builder with the same ID already exists
	 */
	@Nonnull
	public DataBuilder appendBrowserBuilder(@Nonnull final Browser.Builder browserBuilder) {
		Check.notNull(browserBuilder, "browserBuilder");
		Check.notNegative(browserBuilder.getId(), "browserBuilder.getId()");
		if (browserBuilder.getType() == null && browserBuilder.getTypeId() < 0) {
			throw new IllegalStateOfArgumentException("A Type or Type-ID of argument 'browserBuilder' must be set.");
		}
		if (browserBuilders.containsKey(browserBuilder.getId())) {
			throw new IllegalStateOfArgumentException("The browser builder '" + browserBuilder.getProducer() + " "
					+ browserBuilder.getFamily() + "' is already in the map.");
		}

		final Browser.Builder builder = browserBuilder.copy();
		browserBuilders.put(builder.getId(), builder);
		return this;
	}

	@Nonnull
	public DataBuilder appendBrowserOperatingSystemMapping(@Nonnull final BrowserOperatingSystemMapping browserOsMapping) {
		Check.notNull(browserOsMapping, "browserOsMapping");

		browserToOperatingSystemMap.add(browserOsMapping);
		return this;
	}

	/**
	 * Appends a browser pattern to the map of pattern sorted by ID.
	 * 
	 * @param pattern
	 *            a pattern for a browser
	 * @return itself
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the given argument is {@code null}
	 */
	@Nonnull
	public DataBuilder appendBrowserPattern(@Nonnull final BrowserPattern pattern) {
		Check.notNull(pattern, "pattern");
		if (!browserPatterns.containsKey(pattern.getId())) {
			browserPatterns.put(pattern.getId(), new TreeSet<BrowserPattern>(BROWSER_PATTERN_COMPARATOR));
		}

		browserPatterns.get(pattern.getId()).add(pattern);
		return this;
	}

	@Nonnull
	public DataBuilder appendBrowserType(@Nonnull final BrowserType type) {
		Check.notNull(type, "type");

		browserTypes.put(type.getId(), type);
		return this;
	}

	public DataBuilder appendDevice(@Nonnull final Device device) {
		Check.notNull(device, "device");

		devices.add(device);
		return this;
	}

	/**
	 * Appends a copy of the given {@code Device.Builder} to the internal data structure.
	 * 
	 * @param deviceBuilder
	 *            {@code Device.Builder} to be copied and appended
	 * @return this {@code Builder}, for chaining
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the given argument is {@code null}
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if the ID of the given builder is invalid
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if a builder with the same ID already exists
	 */
	@Nonnull
	public DataBuilder appendDeviceBuilder(@Nonnull final Device.Builder deviceBuilder) {
		Check.notNull(deviceBuilder, "deviceBuilder");
		Check.notNegative(deviceBuilder.getId(), "deviceBuilder.getId()");
		if (deviceBuilders.containsKey(deviceBuilder.getId())) {
			throw new IllegalStateOfArgumentException("The device builder '" + deviceBuilder.getName() + "' is already in the map.");
		}

		final Device.Builder builder = deviceBuilder.copy();
		deviceBuilders.put(builder.getId(), builder);
		return this;
	}

	/**
	 * Appends a device pattern to the map of pattern sorted by ID.
	 * 
	 * @param pattern
	 *            a pattern for a device
	 * @return itself
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the given argument is {@code null}
	 */
	@Nonnull
	public DataBuilder appendDevicePattern(@Nonnull final DevicePattern pattern) {
		Check.notNull(pattern, "pattern");
		if (!devicePatterns.containsKey(pattern.getId())) {
			devicePatterns.put(pattern.getId(), new TreeSet<DevicePattern>(DEVICE_PATTERN_COMPARATOR));
		}

		devicePatterns.get(pattern.getId()).add(pattern);
		return this;
	}

	@Nonnull
	public DataBuilder appendOperatingSystem(@Nonnull final OperatingSystem operatingSystem) {
		Check.notNull(operatingSystem, "operatingSystem");

		operatingSystems.add(operatingSystem);
		return this;
	}

	/**
	 * Appends a copy of the given {@code OperatingSystem.Builder} to the internal data structure.
	 * 
	 * @param operatingSystemBuilder
	 *            {@code OperatingSystem.Builder} to be copied and appended
	 * @return this {@code Builder}, for chaining
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the given argument is {@code null}
	 * @throws net.sf.qualitycheck.exception.IllegalNegativeArgumentException
	 *             if the ID of the given builder is negative
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if a builder with the same ID already exists
	 */
	@Nonnull
	public DataBuilder appendOperatingSystemBuilder(@Nonnull final OperatingSystem.Builder operatingSystemBuilder) {
		Check.notNull(operatingSystemBuilder, "operatingSystemBuilder");
		Check.notNegative(operatingSystemBuilder.getId(), "operatingSystemBuilder.getId()");
		Check.stateIsTrue(!operatingSystemBuilders.containsKey(operatingSystemBuilder.getId()),
				"Operating system builder with ID '%s' already exists.", operatingSystemBuilder.getId());

		final OperatingSystem.Builder builder = operatingSystemBuilder.copy();
		operatingSystemBuilders.put(builder.getId(), builder);
		return this;
	}

	/**
	 * Appends an operating system pattern to the map of pattern sorted by ID.
	 * 
	 * @param pattern
	 *            a pattern for a browser
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the pattern is {@code null}
	 * @return itself
	 */
	@Nonnull
	public DataBuilder appendOperatingSystemPattern(@Nonnull final OperatingSystemPattern pattern) {
		Check.notNull(pattern, "pattern");

		if (!operatingSystemPatterns.containsKey(pattern.getId())) {
			operatingSystemPatterns.put(pattern.getId(), new TreeSet<OperatingSystemPattern>(OS_PATTERN_COMPARATOR));
		}

		operatingSystemPatterns.get(pattern.getId()).add(pattern);
		return this;
	}

	@Nonnull
	public DataBuilder appendRobot(@Nonnull final Robot robot) {
		Check.notNull(robot, "robot");

		robots.add(robot);
		return this;
	}

	@Nonnull
	public Data build() {
		addTypeToBrowser(browserBuilders, browserTypes);
		addPatternToBrowser(browserBuilders, browserPatterns);
		addPatternToOperatingSystem(operatingSystemBuilders, operatingSystemPatterns);
		addPatternToDevice(deviceBuilders, devicePatterns);

		final Map<Integer, OperatingSystem> systems = buildOperatingSystems(operatingSystemBuilders);
		addOperatingSystemToBrowser(browserBuilders, systems, convertBrowserOsMapping(browserToOperatingSystemMap));

		final Set<OperatingSystem> osSet = convertOperatingSystems(systems);
		osSet.addAll(operatingSystems);

		final Set<Browser> browserSet = buildBrowsers(browserBuilders);
		browserSet.addAll(browsers);

		final Set<Device> deviceSet = buildDevices(deviceBuilders);
		deviceSet.addAll(devices);

		final SortedMap<BrowserPattern, Browser> patternToBrowserMap = buildPatternToBrowserMap(browserSet);
		final SortedMap<OperatingSystemPattern, OperatingSystem> patternToOperatingSystemMap = buildPatternToOperatingSystemMap(osSet);
		final SortedMap<DevicePattern, Device> patternToDeviceMap = buildPatternToDeviceMap(deviceSet);

		return new Data(browserSet, browserPatterns, browserTypes, patternToBrowserMap, browserToOperatingSystemMap, osSet,
				operatingSystemPatterns, patternToOperatingSystemMap, robots, deviceSet, devicePatterns, patternToDeviceMap, version);
	}

	@Nonnull
	public DataBuilder setVersion(@Nonnull final String version) {
		Check.notNull(version, "version");

		this.version = version;
		return this;
	}

}