/* * Copyright (C) 2013 The Android Open Source Project * * 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 com.android.navigation; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Map; public class XMLWriter { public static final String UNDEFINED = null; private final Map<Class, Properties.Property[]> classToProperties = new IdentityHashMap<Class, Properties.Property[]>(); private final PrintStream out; private int level; private Map<Class, Integer> idCounts = new IdentityHashMap<Class, Integer>(); public XMLWriter(OutputStream out) { this.out = new PrintStream(out); } private static boolean isPrimitive(Class type) { return type.isPrimitive() || type == String.class; } private String nextId(Class c) { Integer prev = idCounts.get(c); if (prev == null) { prev = -1; } int result = prev + 1; idCounts.put(c, result); return Utilities.decapitalize(c.getSimpleName()) + result; } private Map<Object, Info> objectToInfo = new IdentityHashMap<Object, Info>() { @Override public Info get(Object o) { Info result = super.get(o); if (result == null) { put(o, result = new Info()); } return result; } }; static class Info { String id = UNDEFINED; int count = 0; } abstract class NameValue<T> { public final String name; public final T value; NameValue(String name, T value) { this.name = name; this.value = value; } public abstract void write(); public abstract void addToParent(Element parent); } class Attribute<T> extends NameValue<T> { Attribute(String name, T value) { super(name, value); } @Override public void write() { writeAttribute(name, value); } @Override public void addToParent(Element parent) { parent.attributes.add(this); } } class ClassAttribute extends Attribute<Class> { public ClassAttribute(String name, Class value) { super(name, value); } /** * This method is called when we are considering whether to add a class attribute to, for example, * the value of the "state" property of a {@link Transition}. If the property is "get/setState()" we needn't record * the fully qualified "State" class as {@link XMLReader} can be infer it from the type of the getter/setter pair. */ @Override public void addToParent(Element parent) { if (parent.type != value) { if (parent.tag == null) { parent.tag = Utilities.decapitalize(value.getSimpleName()); } else { super.addToParent(parent); } } } @Override public void write() { writeAttribute(name, value.getName()); } } class Element extends NameValue<Object> { public String tag; public final Class type; public final ArrayList<Attribute> attributes = new ArrayList<Attribute>(); public final ArrayList<Element> elements = new ArrayList<Element>(); Element(Class type, String name, Object value) { super(name, value); this.tag = name; this.type = type; } @Override public void write() { level++; String tag = this.tag == null ? "object" : this.tag; print("<" + tag); Info info = objectToInfo.get(value); if (info.count > 1) { if (info.id == UNDEFINED) { writeAttribute(Utilities.ID_ATTRIBUTE_NAME, info.id = nextId(value.getClass())); } else { writeAttribute(Utilities.IDREF_ATTRIBUTE_NAME, info.id); } } for (Attribute attribute : attributes) { attribute.write(); } if (!elements.isEmpty()) { out.println(">"); for (Element element : elements) { element.write(); } println("</" + tag + ">"); } else { out.println("/>"); } level--; } @Override public void addToParent(Element parent) { parent.elements.add(this); } } public Properties.Property[] getProperties(Class c) { Properties.Property[] result = classToProperties.get(c); if (result == null) { classToProperties.put(c, result = Properties.computeProperties(c)); } return result; } public void write(Object o) { println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); level = -1; NameValue traversal = traverse(Object.class, null, o, true); traversal.write(); } private void indent() { for (int i = 0; i < level; i++) { out.write(' '); out.write(' '); } } private void print(String s) { indent(); out.print(s); } private void println(String s) { indent(); out.println(s); } private void writeAttribute(String name, Object value) { print("\n"); level++; print(name + " = \"" + value + "\""); level--; } private NameValue traverse(@NonNull Class type, String name, Object value, boolean isTopLevel) { if (isPrimitive(type)) { return new Attribute<Object>(name, value); } if (type == Class.class) { return new ClassAttribute(name, (Class)value); } Element result = new Element(type, name, value); Class aClass = value.getClass(); if (isTopLevel) { String packageName = aClass.getPackage().getName(); // todo deal with multiple packages result.attributes.add(new Attribute<Object>(Utilities.NAME_SPACE_ATRIBUTE_NAME, "http://schemas.android.com?import=" + packageName + ".*")); } Info info = objectToInfo.get(value); info.count++; if (info.count > 1) { return result; } // recurse into each property, using reflection for (Properties.Property p : getProperties(aClass)) { try { Object propertyValue = p.getValue(value); if (propertyValue != null) { traverse(p.getType(), p.getName(), propertyValue, false).addToParent(result); } } catch (Properties.PropertyAccessException e) { throw new RuntimeException(e); } } // special-case Collections if (value instanceof Collection) { for (Object element : (Collection)value) { traverse(Object.class, null, element, false).addToParent(result); } } return result; } }