/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.googlecode.openbeans; import com.googlecode.openbeans.Encoder; import com.googlecode.openbeans.Expression; import com.googlecode.openbeans.Statement; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import org.apache.harmony.beans.BeansUtils; import org.apache.harmony.beans.internal.nls.Messages; /** * <code>XMLEncoder</code> extends <code>Encoder</code> to write out the encoded * statements and expressions in XML format. The XML can be read by * <code>XMLDecoder</code> later to restore objects and their states. * <p> * The API is similar to <code>ObjectOutputStream</code>. * </p> * */ public class XMLEncoder extends Encoder { private static final String DEFAULT_ENCODING = "UTF-8"; //$NON-NLS-1$ private static int DEADLOCK_THRESHOLD = 7; /* * Every object written by the encoder has a record. */ private static class Record { // The expression by which the object is created or obtained. Expression exp = null; // Id of the object, if it is referenced more than once. String id = null; // Count of the references of the object. int refCount = 0; // A list of statements that execute on the object. ArrayList<Statement> stats = new ArrayList<Statement>(); } private static final int INDENT_UNIT = 1; private static final boolean isStaticConstantsSupported = true; // the main record of all root objects private ArrayList<Object> flushPending = new ArrayList<Object>(); // the record of root objects with a void tag private ArrayList<Object> flushPendingStat = new ArrayList<Object>(); // keep the pre-required objects for each root object private ArrayList<Object> flushPrePending = new ArrayList<Object>(); private boolean hasXmlHeader = false; /* * if any expression or statement references owner, it is set true in method * recordStatement() or recordExpressions(), and, at the first time * flushObject() meets an owner object, it calls the flushOwner() method and * then set needOwner to false, so that all succeeding flushing of owner * will call flushExpression() or flushStatement() normally, which will get * a reference of the owner property. */ private boolean needOwner = false; private PrintWriter out; private Object owner = null; private IdentityHashMap<Object, Record> objRecordMap = new IdentityHashMap<Object, Record>(); private IdentityHashMap<Class<?>, Integer> clazzCounterMap = new IdentityHashMap<Class<?>, Integer>(); private IdentityHashMap<Object, ArrayList<Object>> objPrePendingCache = new IdentityHashMap<Object, ArrayList<Object>>(); private boolean writingObject = false; /** * Construct a <code>XMLEncoder</code>. * * @param out * the output stream where XML is written to */ public XMLEncoder(OutputStream out) { if (null != out) { try { this.out = new PrintWriter(new OutputStreamWriter(out, DEFAULT_ENCODING), true); } catch (UnsupportedEncodingException e) { // should never occur } } } /** * Call <code>flush()</code> first, then write out XML footer and close the * underlying output stream. */ public void close() { flush(); out.println("</java> "); //$NON-NLS-1$ out.close(); } private StringBuffer decapitalize(String s) { StringBuffer buf = new StringBuffer(s); buf.setCharAt(0, Character.toLowerCase(buf.charAt(0))); return buf; } /** * Writes out all objects since last flush to the output stream. * <p> * The implementation write the XML header first if it has not been written. * Then all pending objects since last flush are written. * </p> */ @SuppressWarnings("nls") public void flush() { synchronized (this) { // write xml header if (!hasXmlHeader) { out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?> "); out.println("<java version=\"" + System.getProperty("java.version") + "\" class=\"com.googlecode.openbeans.XMLDecoder\"> "); hasXmlHeader = true; } // preprocess pending objects for (Iterator<Object> iter = flushPending.iterator(); iter .hasNext();) { Object o = iter.next(); Record rec = objRecordMap.get(o); if (rec != null) { preprocess(o, rec); } } // flush pending objects for (Iterator<Object> iter = flushPending.iterator(); iter .hasNext();) { Object o = iter.next(); flushObject(o, INDENT_UNIT); // remove flushed obj iter.remove(); } // clear statement records objRecordMap.clear(); flushPendingStat.clear(); objPrePendingCache.clear(); clazzCounterMap.clear(); // remove all old->new mappings super.clear(); } } @SuppressWarnings("nls") private void flushBasicObject(Object obj, int indent) { if (obj instanceof Proxy) { return; } flushIndent(indent); if (obj == null) { out.println("<null /> "); } else if (obj instanceof String) { Record rec = objRecordMap.get(obj); if (null != rec) { flushExpression(obj, rec, indent - 3, flushPendingStat.contains(obj)); return; } out.print("<string>"); flushString((String) obj); out.println("</string> "); } else if (obj instanceof Class<?>) { out.println("<class>" + ((Class<?>) obj).getName() + "</class> "); } else if (obj instanceof Boolean) { out.println("<boolean>" + obj + "</boolean> "); } else if (obj instanceof Byte) { out.println("<byte>" + obj + "</byte> "); } else if (obj instanceof Character) { char objChar = ((Character) obj).charValue(); if (invalidCharacter(objChar)) { out.println("<char code=\"#" + Integer.toString(objChar, 16) + "\"/>"); } else { out.println("<char>" + objChar + "</char> "); } } else if (obj instanceof Double) { out.println("<double>" + obj + "</double> "); } else if (obj instanceof Float) { out.println("<float>" + obj + "</float> "); } else if (obj instanceof Integer) { out.println("<int>" + obj + "</int> "); } else if (obj instanceof Long) { out.println("<long>" + obj + "</long> "); } else if (obj instanceof Short) { out.println("<short>" + obj + "</short> "); } else { getExceptionListener().exceptionThrown( new Exception(Messages.getString("beans.73", obj))); } } private boolean invalidCharacter(char c) { return ((0x0000 <= c && c < 0x0009) || (0x000a < c && c < 0x000d) || (0x000d < c && c < 0x0020) || (0xd7ff < c && c < 0xe000) || c == 0xfffe); } @SuppressWarnings("nls") private void flushExpression(Object obj, Record rec, int indent, boolean asStatement) { // flush Statement stat = asStatement ? new Statement(rec.exp.getTarget(), rec.exp.getMethodName(), rec.exp.getArguments()) : rec.exp; if (isStaticConstantsSupported && "getField".equals(stat.getMethodName())) { flushStatField(stat, indent); return; } // not first time, use idref if (rec.id != null) { flushIndent(indent); out.print("<object idref=\""); out.print(rec.id); out.println("\"/> "); return; } // generate id, if necessary if (rec.refCount > 1 && rec.id == null) { rec.id = idSerialNoOfObject(obj); } // flush flushStatement(stat, rec.id, rec.stats, indent); } private void flushIndent(int indent) { for (int i = 0; i < indent; i++) { out.print(' '); } } private void flushObject(Object obj, int indent) { Record rec = objRecordMap.get(obj); if (rec == null && !isBasicType(obj)) { return; } if (obj == owner && this.needOwner) { flushOwner(obj, rec, indent); this.needOwner = false; return; } if (isBasicType(obj)) { flushBasicObject(obj, indent); } else { flushExpression(obj, rec, indent, flushPendingStat.contains(obj)); } } @SuppressWarnings("nls") private void flushOwner(Object obj, Record rec, int indent) { if (rec.refCount > 1 && rec.id == null) { rec.id = idSerialNoOfObject(obj); } flushIndent(indent); String tagName = "void"; out.print("<"); out.print(tagName); // id attribute if (rec.id != null) { out.print(" id=\""); out.print(rec.id); out.print("\""); } out.print(" property=\"owner\""); // open tag, end if (rec.exp.getArguments().length == 0 && rec.stats.isEmpty()) { out.println("/> "); return; } out.println("> "); // arguments for (int i = 0; i < rec.exp.getArguments().length; i++) { flushObject(rec.exp.getArguments()[i], indent + INDENT_UNIT); } // sub statements flushSubStatements(rec.stats, indent); // close tag flushIndent(indent); out.print("</"); out.print(tagName); out.println("> "); } @SuppressWarnings("nls") private void flushStatArray(Statement stat, String id, List<?> subStats, int indent) { // open tag, begin flushIndent(indent); out.print("<array"); // id attribute if (id != null) { out.print(" id=\""); out.print(id); out.print("\""); } // class & length out.print(" class=\""); out.print(((Class<?>) stat.getArguments()[0]).getName()); out.print("\" length=\""); out.print(stat.getArguments()[1]); out.print("\""); // open tag, end if (subStats.isEmpty()) { out.println("/> "); return; } out.println("> "); // sub statements flushSubStatements(subStats, indent); // close tag flushIndent(indent); out.println("</array> "); } @SuppressWarnings("nls") private void flushStatCommon(Statement stat, String id, List<?> subStats, int indent) { // open tag, begin flushIndent(indent); String tagName = stat instanceof Expression ? "object" : "void"; out.print("<"); out.print(tagName); // id attribute if (id != null) { out.print(" id=\""); out.print(id); out.print("\""); } // special class attribute if (stat.getTarget() instanceof Class<?>) { out.print(" class=\""); out.print(((Class<?>) stat.getTarget()).getName()); out.print("\""); } // method attribute if (!"new".equals(stat.getMethodName())) { out.print(" method=\""); out.print(stat.getMethodName()); out.print("\""); } // open tag, end if (stat.getArguments().length == 0 && subStats.isEmpty()) { out.println("/> "); return; } out.println("> "); // arguments for (int i = 0; i < stat.getArguments().length; i++) { flushObject(stat.getArguments()[i], indent + INDENT_UNIT); } // sub statements flushSubStatements(subStats, indent); // close tag flushIndent(indent); out.print("</"); out.print(tagName); out.println("> "); } @SuppressWarnings("nls") private void flushStatement(Statement stat, String id, List<?> subStats, int indent) { Object target = stat.getTarget(); String method = stat.getMethodName(); Object args[] = stat.getArguments(); // special case for array if (Array.class == target && BeansUtils.NEWINSTANCE.equals(method)) { flushStatArray(stat, id, subStats, indent); return; } // special case for get(int) and set(int, Object) if (isGetArrayStat(target, method, args) || isSetArrayStat(target, method, args)) { flushStatIndexed(stat, id, subStats, indent); return; } // special case for getProperty() and setProperty(Object) if (isGetPropertyStat(method, args) || isSetPropertyStat(method, args)) { flushStatGetterSetter(stat, id, subStats, indent); return; } if (isStaticConstantsSupported && "getField".equals(stat.getMethodName())) { flushStatField(stat, indent); return; } // common case flushStatCommon(stat, id, subStats, indent); } @SuppressWarnings("nls") private void flushStatField(Statement stat, int indent) { // open tag, begin flushIndent(indent); out.print("<object"); // special class attribute Object target = stat.getTarget(); if (target instanceof Class<?>) { out.print(" class=\""); out.print(((Class<?>) target).getName()); out.print("\""); } Field field = null; if (target instanceof Class<?> && stat.getArguments().length == 1 && stat.getArguments()[0] instanceof String) { try { field = ((Class<?>) target).getField((String) stat .getArguments()[0]); } catch (Exception e) { // ignored } } if (field != null && Modifier.isStatic(field.getModifiers())) { out.print(" field=\""); out.print(stat.getArguments()[0]); out.print("\""); out.println("/> "); } else { out.print(" method=\""); out.print(stat.getMethodName()); out.print("\""); out.println("> "); flushObject(stat.getArguments()[0], indent + INDENT_UNIT); flushIndent(indent); out.println("</object> "); } } @SuppressWarnings("nls") private void flushStatGetterSetter(Statement stat, String id, List<?> subStats, int indent) { // open tag, begin flushIndent(indent); String tagName = "void"; out.print("<"); out.print(tagName); // id attribute if (id != null) { out.print(" id=\""); out.print(id); out.print("\""); } // special class attribute if (stat.getTarget() instanceof Class<?>) { out.print(" class=\""); out.print(((Class<?>) stat.getTarget()).getName()); out.print("\""); } // property attribute out.print(" property=\""); out.print(decapitalize(stat.getMethodName().substring(3))); out.print("\""); // open tag, end if (stat.getArguments().length == 0 && subStats.isEmpty()) { out.println("/> "); return; } out.println("> "); // arguments for (int i = 0; i < stat.getArguments().length; i++) { flushObject(stat.getArguments()[i], indent + INDENT_UNIT); } // sub statements flushSubStatements(subStats, indent); // close tag flushIndent(indent); out.print("</"); out.print(tagName); out.println("> "); } @SuppressWarnings("nls") private void flushStatIndexed(Statement stat, String id, List<?> subStats, int indent) { // open tag, begin flushIndent(indent); String tagName = stat instanceof Expression ? "object" : "void"; out.print("<"); out.print(tagName); // id attribute if (id != null) { out.print(" id=\""); out.print(id); out.print("\""); } // special class attribute if (stat.getTarget() instanceof Class<?>) { out.print(" class=\""); out.print(((Class<?>) stat.getTarget()).getName()); out.print("\""); } // index attribute out.print(" index=\""); out.print(stat.getArguments()[0]); out.print("\""); // open tag, end if (stat.getArguments().length == 1 && subStats.isEmpty()) { out.println("/> "); return; } out.println("> "); // arguments for (int i = 1; i < stat.getArguments().length; i++) { flushObject(stat.getArguments()[i], indent + INDENT_UNIT); } // sub statements flushSubStatements(subStats, indent); // close tag flushIndent(indent); out.print("</"); out.print(tagName); out.println("> "); } @SuppressWarnings("nls") private void flushString(String s) { char c; for (int i = 0; i < s.length(); i++) { c = s.charAt(i); if (c == '<') { out.print("<"); } else if (c == '>') { out.print(">"); } else if (c == '&') { out.print("&"); } else if (c == '\'') { out.print("'"); } else if (c == '"') { out.print("""); } else { if (invalidCharacter(c)) { out.print("<char code=\"#" + Integer.toString(c, 16) + "\"/>"); } else { out.print(c); } } } } private void flushSubStatements(List<?> subStats, int indent) { for (int i = 0; i < subStats.size(); i++) { Statement subStat = (Statement) subStats.get(i); try { if (subStat.getClass() == Expression.class) { Expression subExp = (Expression) subStat; Object obj = subExp.getValue(); Record rec = objRecordMap.get(obj); flushExpression(obj, rec, indent + INDENT_UNIT, true); } else { flushStatement(subStat, null, Collections.EMPTY_LIST, indent + INDENT_UNIT); } } catch (Exception e) { // should not happen getExceptionListener().exceptionThrown(e); } } } /** * Returns the owner of this encoder. * * @return the owner of this encoder */ public Object getOwner() { return owner; } private boolean isBasicType(Object value) { return value == null || value instanceof Boolean || value instanceof Byte || value instanceof Character || value instanceof Class<?> || value instanceof Double || value instanceof Float || value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof String || value instanceof Proxy; } private boolean isGetArrayStat(Object target, String method, Object[] args) { return (BeansUtils.GET.equals(method) && args.length == 1 && args[0] instanceof Integer && target.getClass().isArray()); } private boolean isGetPropertyStat(String method, Object[] args) { return (method.startsWith(BeansUtils.GET) && method.length() > 3 && args.length == 0); } private boolean isSetArrayStat(Object target, String method, Object[] args) { return (BeansUtils.SET.equals(method) && args.length == 2 && args[0] instanceof Integer && target.getClass().isArray()); } private boolean isSetPropertyStat(String method, Object[] args) { return (method.startsWith(BeansUtils.SET) && method.length() > 3 && args.length == 1); } private String idSerialNoOfObject(Object obj) { Class<?> clazz = obj.getClass(); Integer serialNo = (Integer) clazzCounterMap.get(clazz); serialNo = serialNo == null ? 0 : serialNo; String id = BeansUtils.idOfClass(obj.getClass()) + serialNo; clazzCounterMap.put(clazz, ++serialNo); return id; } /* * The preprocess removes unused statements and counts references of every * object */ private void preprocess(Object obj, Record rec) { if (writingObject && isBasicType(obj)) { return; } if (obj instanceof Class<?>) { return; } // count reference rec.refCount++; // do things only one time for each record if (rec.refCount > 1) { return; } // do it recursively if (null != rec.exp) { // deal with 'field' property Record targetRec = objRecordMap.get(rec.exp.getTarget()); if (targetRec != null && targetRec.exp != null && "getField".equals(targetRec.exp.getMethodName())) { objRecordMap.remove(obj); } Object args[] = rec.exp.getArguments(); for (int i = 0; i < args.length; i++) { Record argRec = objRecordMap.get(args[i]); if (argRec != null) { preprocess(args[i], argRec); } } } for (Iterator<?> iter = rec.stats.iterator(); iter.hasNext();) { Statement subStat = (Statement) iter.next(); if (subStat.getClass() == Expression.class) { try { Expression subExp = (Expression) subStat; Record subRec = objRecordMap.get(subExp.getValue()); if (subRec == null || subRec.exp == null || subRec.exp != subExp) { iter.remove(); continue; } preprocess(subExp.getValue(), subRec); if (subRec.stats.isEmpty()) { if (isGetArrayStat(subExp.getTarget(), subExp.getMethodName(), subExp.getArguments()) || isGetPropertyStat(subExp.getMethodName(), subExp.getArguments())) { iter.remove(); continue; } } } catch (Exception e) { getExceptionListener().exceptionThrown(e); iter.remove(); } continue; } Object subStatArgs[] = subStat.getArguments(); for (int i = 0; i < subStatArgs.length; i++) { Record argRec = objRecordMap.get(subStatArgs[i]); if (argRec != null) { preprocess(subStatArgs[i], argRec); } } } } private void recordExpression(Object value, Expression exp) { // record how a new object is created or obtained Record rec = objRecordMap.get(value); if (rec == null) { rec = new Record(); objRecordMap.put(value, rec); } if (rec.exp == null) { // it is generated by its sub statements for (Statement statement : rec.stats) { if (statement.getClass() == Expression.class) { flushPrePending.add(value); } } } rec.exp = exp; // deal with 'owner' property if (value == owner && owner != null) { needOwner = true; } // also record as a statement recordStatement(exp); } private void recordStatement(Statement stat) { if (null == stat) { return; } // deal with 'owner' property Object target = stat.getTarget(); if (target == owner && owner != null) { needOwner = true; } // record how a statement affects the target object Record rec = objRecordMap.get(target); if (rec == null) { rec = new Record(); objRecordMap.put(target, rec); } boolean hasRecord = false; String methodName = stat.getMethodName(); Object[] args = stat.getArguments(); if (isSetPropertyStat(methodName, args) || isSetArrayStat(target, methodName, args)) { for (Statement subStat : rec.stats) { if (target == subStat.getTarget() && methodName.equals(subStat.getMethodName())) { Object[] subArgs = subStat.getArguments(); if (args.length == subArgs.length) { boolean equals = true; for (int index = 0; index < args.length; index++) { if (getPersistenceDelegate(args[index].getClass()) .mutatesTo(args[index], subArgs[index])) { continue; } equals = false; break; } if (equals) { hasRecord = true; break; } } } } } if (!hasRecord) { rec.stats.add(stat); } } /** * Imperfect attempt to detect a dead loop. This works with specific * patterns that can be found in our AWT implementation. See HARMONY-5707 * for details. * * @param value * the object to check dupes for * @return true if a dead loop detected; false otherwise FIXME */ private boolean checkDeadLoop(Object value) { int n = 0; Object obj = value; while (obj != null) { Record rec = objRecordMap.get(obj); if (rec != null && rec.exp != null) { obj = rec.exp.getTarget(); } else { break; } if (obj != null && (obj.getClass().isAssignableFrom(value.getClass())) && obj.equals(value)) { n++; if (n >= DEADLOCK_THRESHOLD) { // System.out.println("Dead loop hit!"); return true; } } } return false; } /** * Sets the owner of this encoder. * * @param owner * the owner to set */ public void setOwner(Object owner) { this.owner = owner; } /** * Records the expression so that it can be written out later, then calls * super implementation. */ @Override public void writeExpression(Expression oldExp) { if (null == oldExp) { throw new NullPointerException(); } boolean oldWritingObject = writingObject; writingObject = true; // get expression value Object oldValue = expressionValue(oldExp); // check existence if (oldValue == null || get(oldValue) != null && (oldWritingObject || oldValue.getClass() != String.class)) { return; } // record how the object is obtained if (!isBasicType(oldValue) || (!oldWritingObject && oldValue.getClass() == String.class)) { recordExpression(oldValue, oldExp); } // try to detect if we run into a dead loop if (checkDeadLoop(oldValue)) { return; } super.writeExpression(oldExp); writingObject = oldWritingObject; } /** * Records the object so that it can be written out later, then calls super * implementation. */ @Override public void writeObject(Object o) { synchronized (this) { ArrayList<Object> prePending = objPrePendingCache.get(o); if (prePending == null) { boolean oldWritingObject = writingObject; writingObject = true; try { super.writeObject(o); } finally { writingObject = oldWritingObject; } } else { flushPrePending.clear(); flushPrePending.addAll(prePending); } // root object if (!writingObject) { boolean isNotCached = prePending == null; // is not cached, add to cache if (isNotCached && o != null) { prePending = new ArrayList<Object>(); prePending.addAll(flushPrePending); objPrePendingCache.put(o, prePending); } // add to pending flushPending.addAll(flushPrePending); flushPendingStat.addAll(flushPrePending); flushPrePending.clear(); if (isNotCached && flushPending.contains(o)) { flushPendingStat.remove(o); } else { flushPending.add(o); } if (needOwner) { this.flushPending.remove(owner); this.flushPending.add(0, owner); } } } } /** * Records the statement so that it can be written out later, then calls * super implementation. */ @Override public void writeStatement(Statement oldStat) { if (null == oldStat) { System.err .println("java.lang.Exception: XMLEncoder: discarding statement null"); System.err.println("Continuing..."); return; } // record how the object is changed recordStatement(oldStat); super.writeStatement(oldStat); } }