/* * Copyright (C) 2016 MegaMek team * * This file is part of MekHQ. * * MekHQ is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * MekHQ 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with MekHQ. If not, see <http://www.gnu.org/licenses/>. */ package mekhq.campaign; import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import org.joda.time.DateTime; import org.w3c.dom.Node; import mekhq.MekHQ; /** * Class for holding extra data/properties with free-form strings as keys. * <p> * Example usage: * <p> * - creating keys * <pre> * ExtraData.Key<Integer> INTKEY = new ExtraData.IntKey("int_key"); * ExtraData.Key<Double> DOUBLEKEY = new ExtraData.DoubleKey("double_key"); * ExtraData.Key<DateTime> DATEKEY = new ExtraData.DateKey("current date"); * ExtraData.Key<Boolean> BOOLEANKEY = new ExtraData.BooleanKey("realy?"); * ExtraData.Key<String> PLAIN_OLD_BORING_KEY = new ExtraData.StringKey("stuff"); * </pre> * - setting and getting data * <pre> * ed.set(INTKEY, 75); * ed.set(DOUBLEKEY, 12.5); * ed.set(DATEKEY, new DateTime()); * Integer intVal = ed.get(INTKEY)); * Double doubleVal = ed.get(DOUBLEKEY)); * DateTime date = ed.get(DATEKEY)); * // the next one guarantees to not return null, but -1 if the value is not set * int anotherIntVal = ed.get(INTKEY, -1); * </pre> * - saving to XML and creating from XML * <pre> * ed.writeToXml(System.out); * ExtraData newEd = ExtraData.createFromXml(xmlNode); * </pre> */ @XmlRootElement(name="extraData") @XmlAccessorType(XmlAccessType.FIELD) public class ExtraData { private static final Marshaller marshaller; private static final Unmarshaller unmarshaller; static { Marshaller m = null;; Unmarshaller u = null; try { JAXBContext context = JAXBContext.newInstance(ExtraData.class); m = context.createMarshaller(); m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); u = context.createUnmarshaller(); } catch(Exception ex) { MekHQ.logError(ex); } marshaller = m; unmarshaller = u; } private static final Map<Class<?>, StringAdapter<?>> ADAPTERS = new HashMap<>(); static { ADAPTERS.put(String.class, new StringAdapter<String>() { @Override public String adapt(String str) { return str; } }); ADAPTERS.put(Integer.class, new StringAdapter<Integer>() { @Override public Integer adapt(String str) { return Integer.valueOf(str); } }); ADAPTERS.put(Double.class, new StringAdapter<Double>() { @Override public Double adapt(String str) { return Double.valueOf(str); } }); ADAPTERS.put(Boolean.class, new StringAdapter<Boolean>() { @Override public Boolean adapt(String str) { return Boolean.valueOf(str); } }); ADAPTERS.put(DateTime.class, new StringAdapter<DateTime>() { @Override public DateTime adapt(String str) { return new DateTime(str); } }); } @XmlElement(name="map") @XmlJavaTypeAdapter(JAXBValueAdapter.class) private Map<Class<?>, Map<String, Object>> values = new HashMap<>(); private Map<String, Object> getOrCreateClassMap(Class<?> cls) { return ExtraData.getOrCreateClassMap(values, cls); } /** * Set the given value for the given key. * * @return The previous value if there was one. */ public <T> T set(Key<T> key, T value) { if(null == key) { return null; } Map<String, Object> map = getOrCreateClassMap(key.type); return key.type.cast(map.put(key.name, value)); } /** * Set the given value parsed from the string for the given key, if possible. * * @return The previous value if there was one. */ public <T> T setString(Key<T> key, String value) { if(null == key) { return null; } // Prevent unneeded loops and lookups for straight strings if(key.type == String.class) { Map<String, Object> map = getOrCreateClassMap(key.type); return key.type.cast(map.put(key.name, value)); } return set(key, key.fromString(value)); } /** * @return the value associated with the given key, or <code>null</code> if there isn't one */ public <T> T get(Key<T> key) { if(!values.containsKey(key.type)) { return null; } return key.type.cast(values.get(key.type).get(key.name)); } /** * @return the value associated with the given key, or the default value if there isn't one */ public <T> T get(Key<T> key, T defaultValue) { T result = get(key); return (null != result) ? result : defaultValue; } public void writeToXml(Writer writer) { try { marshaller.marshal(this, writer); } catch(JAXBException e) { MekHQ.logError(e); } } public void writeToXml(OutputStream os) { try { marshaller.marshal(this, os); } catch(JAXBException e) { MekHQ.logError(e); } } public static ExtraData createFromXml(Node wn) { try { return (ExtraData) unmarshaller.unmarshal(wn); } catch(JAXBException e) { MekHQ.logError(e); return null; } } public static ExtraData createFromXml(InputStream is) { try { return (ExtraData) unmarshaller.unmarshal(is); } catch(JAXBException e) { MekHQ.logError(e); return null; } } private static Map<String, Object> getOrCreateClassMap(Map<Class<?>, Map<String, Object>> baseMap, Class<?> cls) { Map<String, Object> map = baseMap.get(cls); if(null == map) { map = new HashMap<>(); baseMap.put(cls, map); } return map; } // XML marshalling/unmarshalling support classes and methods /** * Register an adapter translating from String to the given value. * Already existing adapters are not overwritten. */ public static <T> void registerAdapter(Class<T> cls, StringAdapter<T> adapter) { if((null != cls) && (null != adapter) && !ADAPTERS.containsKey(cls)) { ADAPTERS.put(cls, adapter); } } private static <T> T adapt(Class<T> cls, String val) { if(!ADAPTERS.containsKey(cls)) { return null; } try { return cls.cast(ADAPTERS.get(cls).adapt(val)); } catch(ClassCastException cce) { return null; } } private static <T> String toString(T val) { if(null == val) { return null; } if(!ADAPTERS.containsKey(val.getClass())) { return val.toString(); } @SuppressWarnings("unchecked") StringAdapter<T> adapter = (StringAdapter<T>) ADAPTERS.get(val.getClass()); return adapter.toString(val); } public static abstract class StringAdapter<T> { public abstract T adapt(String str); public String toString(T val) { return (null != val) ? val.toString() : null; } } private static class JAXBValueAdapter extends XmlAdapter<XmlValueListArray, Map<Class<?>, Map<String, Object>>> { @Override public Map<Class<?>, Map<String, Object>> unmarshal(XmlValueListArray v) throws Exception { if((null == v) || (null == v.list) || v.list.isEmpty()) { return null; } Map<Class<?>, Map<String, Object>> result = new HashMap<>(); for(XmlValueList list : v.list) { if(null == list.type) { continue; } Class<?> type = null; try { type = Class.forName(list.type); } catch(ClassNotFoundException cnfe) {} if(null == type) { continue; } Map<String, Object> map = ExtraData.getOrCreateClassMap(result, type); for(XmlValueEntry item : list.entries) { if((null != item) && (null != item.key) && (null != item.value)) { map.put(item.key, adapt(type, item.value)); } } } return result; } @Override public XmlValueListArray marshal(Map<Class<?>, Map<String, Object>> v) throws Exception { if((null == v) || v.isEmpty()) { return null; } ArrayList<XmlValueList> result = new ArrayList<>(); for(Entry<Class<?>, Map<String, Object>> entry : v.entrySet()) { Map<String, Object> value = entry.getValue(); if((null == value) || value.isEmpty()) { continue; } XmlValueList val = new XmlValueList(); val.type = entry.getKey().getName(); val.entries = new ArrayList<>(); for(Entry<String, Object> data : value.entrySet()) { if(null != data.getValue()) { XmlValueEntry newEntry = new XmlValueEntry(); newEntry.key = data.getKey(); newEntry.value = ExtraData.toString(data.getValue()); val.entries.add(newEntry); } } if(!val.entries.isEmpty()) { result.add(val); } } XmlValueListArray arrayResult = new XmlValueListArray(); if(!result.isEmpty()) { arrayResult.list = result; } return arrayResult; } } private static class XmlValueListArray { public List<XmlValueList> list; } private static class XmlValueList { @XmlAttribute public String type; @XmlElement(name="entry") public List<XmlValueEntry> entries; } private static class XmlValueEntry { @XmlAttribute public String key; @XmlAttribute public String value; } // Predefined key types public static abstract class Key<T> { private final String name; private final Class<T> type; protected Key(String name, Class<T> type) { this.name = name; this.type = type; } public String getName() { return name; } public Class<T> getType() { return type; } public T fromString(String str) { return ExtraData.adapt(type, str); } public String toString(T val) { return (null != val) ? val.toString() : null; } @Override public int hashCode() { return Objects.hash(name, type); } @Override public boolean equals(Object object) { if(this == object) { return true; } if((null == object) || (getClass() != object.getClass())) { return false; } @SuppressWarnings("unchecked") final Key<T> other = (Key<T>) object; return Objects.equals(name, other.name) && (type == other.type); } } /** A key referencing a String value */ public static class StringKey extends Key<String> { public StringKey(String name) { super(name, String.class); } } /** A key referencing an Integer or int value */ public static class IntKey extends Key<Integer> { public IntKey(String name) { super(name, Integer.class); } } /** A key referencing a Double or double value */ public static class DoubleKey extends Key<Double> { public DoubleKey(String name) { super(name, Double.class); } } /** A key referencing a Boolean or boolean value */ public static class BooleanKey extends Key<Boolean> { public BooleanKey(String name) { super(name, Boolean.class); } } /** A key referencing a joda-time DateTime value */ public static class DateKey extends Key<DateTime> { public DateKey(String name) { super(name, DateTime.class); } } }