package org.openntf.domino.nsfdata.structs; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import javolution.io.Struct; import com.ibm.commons.util.StringUtil; public abstract class AbstractStruct extends Struct implements Externalizable { public void init() { setByteBuffer(ByteBuffer.allocate(size()).order(ByteOrder.LITTLE_ENDIAN), 0); } public void init(final ByteBuffer data) { setByteBuffer(data.duplicate().order(ByteOrder.LITTLE_ENDIAN), data.position()); } @Override public boolean isPacked() { return true; } @Override public ByteOrder byteOrder() { return ByteOrder.LITTLE_ENDIAN; } public ByteBuffer getData() { return getByteBuffer(); } protected void setData(final byte[] data) { setByteBuffer(ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN), 0); } protected void setData(final ByteBuffer data) { setByteBuffer(data.duplicate().order(ByteOrder.LITTLE_ENDIAN), 0); } public long getPayload(final byte[] result, final long offset) { ByteBuffer data = getData().duplicate(); data.position(data.position() + size()); long length = Math.min(getVariableSize(), result.length - offset); data.get(result, (int) offset, (int) length); return length; } public byte[] getBytes() { ByteBuffer data = getData().duplicate(); // int size = data.limit() - data.position(); byte[] result = new byte[(int) getTotalSize()]; // Ignore extra length when storing data.get(result, 0, (int) (size() + getVariableSize())); return result; } public long getStructSize() { return size(); } public int getExtraLength() { return (int) ((size() + getVariableSize()) % 2); } public long getTotalSize() { long result = size() + getVariableSize() + getExtraLength(); return result; } public long getVariableSize() { long result = 0; Collection<VariableElement> varElements = variableElements_.get(getClass().getName()); if (varElements != null) { for (VariableElement element : varElements) { try { int length = -1; boolean found = false; // The length method name could either be the name of a fixed variable or a method try { Field field = getClass().getDeclaredField(element.lengthMethodName); if (Unsigned8.class.isAssignableFrom(field.getType())) { length = ((Unsigned8) field.get(this)).get(); found = true; } else if (Unsigned16.class.isAssignableFrom(field.getType())) { length = ((Unsigned16) field.get(this)).get(); found = true; } else if (Unsigned32.class.isAssignableFrom(field.getType())) { length = (int) ((Unsigned32) field.get(this)).get(); found = true; } } catch (NoSuchFieldException nsfe) { // Ignore and move on } if (!found) { Method method = getClass().getDeclaredMethod(element.lengthMethodName); length = (Integer) method.invoke(this); } result += length; } catch (Throwable t) { throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); } } } return result; } @Override public String toString() { return buildDebugString(); } @Override public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { int len = in.readInt(); byte[] storage = new byte[len]; in.read(storage); setByteBuffer(ByteBuffer.wrap(storage).order(ByteOrder.LITTLE_ENDIAN), 0); } @Override public void writeExternal(final ObjectOutput out) throws IOException { byte[] storage = getBytes(); // getByteBuffer().get(storage); out.writeInt(storage.length); out.write(storage); } /* ****************************************************************************** * Internal structure methods ********************************************************************************/ private static interface Element { Class<?> getDataClass(); boolean isUpgrade(); boolean isArray(); } /** * This class represents a variable-length element in the static structure definition. * * @author jgallagher * */ private static class VariableElement implements Element { private final String name; private final String lengthMethodName; private final Class<?> dataClass; private final boolean isAscii; private VariableElement(final String name, final String lengthMethodName, final Class<?> dataClass, final boolean isAscii) { this.name = name; this.lengthMethodName = lengthMethodName; this.dataClass = dataClass; this.isAscii = isAscii; } @Override public Class<?> getDataClass() { return dataClass; } @Override public boolean isUpgrade() { return false; } @Override public boolean isArray() { return !String.class.equals(dataClass); } @Override public int hashCode() { return 11 + name.hashCode() + lengthMethodName.hashCode() + dataClass.hashCode() + (isAscii ? 2 : 1); } } private static final Map<String, Collection<VariableElement>> variableElements_ = new HashMap<String, Collection<VariableElement>>(); /** * @param name * The name of the field, used in "getStructElement" calls * @param lengthMethodName * The name of a method that can be called to get a int of the length in bytes */ protected static void addVariableData(final String name, final String lengthMethodName) { Exception e = new Exception(); String caller = e.getStackTrace()[1].getClassName(); if (!variableElements_.containsKey(caller)) { variableElements_.put(caller, new LinkedHashSet<VariableElement>()); } variableElements_.get(caller).add(new VariableElement(name, lengthMethodName, Byte.class, false)); } protected static void addVariableArray(final String name, final String lengthMethodName, final Class<?> dataClass) { Exception e = new Exception(); String caller = e.getStackTrace()[1].getClassName(); if (!variableElements_.containsKey(caller)) { variableElements_.put(caller, new LinkedHashSet<VariableElement>()); } variableElements_.get(caller).add(new VariableElement(name, lengthMethodName, dataClass, false)); } /** * @param name * The name of the field, used in "getStructElement" calls * @param lengthMethodName * The name of a method that can be called to get a int of the length in bytes */ protected static void addVariableString(final String name, final String lengthMethodName) { Exception e = new Exception(); String caller = e.getStackTrace()[1].getClassName(); if (!variableElements_.containsKey(caller)) { variableElements_.put(caller, new LinkedHashSet<VariableElement>()); } variableElements_.get(caller).add(new VariableElement(name, lengthMethodName, String.class, false)); } /** * @param name * The name of the field, used in "getStructElement" calls * @param lengthMethodName * The name of a method that can be called to get a int of the length in bytes */ protected static void addVariableAsciiString(final String name, final String lengthMethodName) { Exception e = new Exception(); String caller = e.getStackTrace()[1].getClassName(); if (!variableElements_.containsKey(caller)) { variableElements_.put(caller, new LinkedHashSet<VariableElement>()); } variableElements_.get(caller).add(new VariableElement(name, lengthMethodName, String.class, true)); } protected Object getVariableElement(final String name) { int preceding = size(); // Now see if it's one of the variable-length bits Collection<VariableElement> varElements = variableElements_.get(getClass().getName()); if (varElements != null) { for (VariableElement element : varElements) { try { int length = -1; boolean found = false; // The length method name could either be the name of a fixed variable or a method try { Field field = getClass().getDeclaredField(element.lengthMethodName); if (Unsigned8.class.isAssignableFrom(field.getType())) { length = ((Unsigned8) field.get(this)).get(); found = true; } else if (Unsigned16.class.isAssignableFrom(field.getType())) { length = ((Unsigned16) field.get(this)).get(); found = true; } else if (Unsigned32.class.isAssignableFrom(field.getType())) { length = (int) ((Unsigned32) field.get(this)).get(); found = true; } } catch (NoSuchFieldException nsfe) { // Ignore and move on } if (!found) { Method method = getClass().getDeclaredMethod(element.lengthMethodName); length = (Integer) method.invoke(this); } int size = String.class.equals(element.dataClass) ? 1 : _getSize(element.dataClass); // LMBCS strings are always even length int extra = String.class.equals(element.dataClass) && !element.isAscii ? length % 2 : 0; // int extra = String.class.equals(element.dataClass) ? length % 2 : 0; if (StringUtil.equals(name, element.name)) { if (String.class.equals(element.dataClass)) { ByteBuffer data = getData().duplicate(); data.order(ByteOrder.LITTLE_ENDIAN); // System.out.println("length for " + name + " is " + length); // System.out.println("setting position to " + (data.position() + preceding)); data.position(data.position() + preceding); // System.out.println("setting limit to " + (data.position() + length)); data.limit(data.position() + length); if (element.isAscii) { byte[] chars = new byte[length]; data.get(chars); return new String(chars, Charset.forName("US-ASCII")); } else { return ODSUtils.fromLMBCS(data); } } else { Object[] result = new Object[length]; for (int i = 0; i < length; i++) { Object primitive = _getPrimitive(element.dataClass, preceding + (size * i), false); if (primitive != null) { result[i] = primitive; } else { // Then it's a struct ByteBuffer data = getData().duplicate(); data.order(ByteOrder.LITTLE_ENDIAN); data.position(data.position() + preceding + (size * i)); data.limit(data.position() + size); try { result[i] = element.dataClass.newInstance(); element.dataClass.getMethod("init", ByteBuffer.class).invoke(result[i], data); } catch (Throwable t) { throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); } } } if (_isPrimitive(element.dataClass)) { return _toPrimitiveArray(result, element.dataClass); } else { // TODO see if there's a better way Object resultArray = Array.newInstance(element.dataClass, length); for (int i = 0; i < length; i++) { Array.set(resultArray, i, result[i]); } return resultArray; } } } else { preceding += (size * length) + extra; } } catch (Throwable t) { throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); } } } return null; } /** * Sets a variable-sized element of the struct to the provided value. If the element's length method name is the name of a field, it * also sets that field to the new length. * * @return The size in bytes of the stored struct element */ protected int setVariableElement(final String name, final Object value) { // System.out.println("setting field " + name + " to " + value); int preceding = size(); // Now see if it's one of the variable-length bits Collection<VariableElement> varElements = variableElements_.get(getClass().getName()); if (varElements != null) { for (VariableElement element : varElements) { try { int length = -1; boolean found = false; // The length method name could either be the name of a fixed variable or a method Field field = null; try { field = getClass().getDeclaredField(element.lengthMethodName); if (Unsigned8.class.isAssignableFrom(field.getType())) { length = ((Unsigned8) field.get(this)).get(); found = true; } else if (Unsigned16.class.isAssignableFrom(field.getType())) { length = ((Unsigned16) field.get(this)).get(); found = true; } else if (Unsigned32.class.isAssignableFrom(field.getType())) { length = (int) ((Unsigned32) field.get(this)).get(); found = true; } } catch (NoSuchFieldException nsfe) { // Ignore and move on } if (!found) { Method method = getClass().getDeclaredMethod(element.lengthMethodName); length = (Integer) method.invoke(this); } int size = String.class.equals(element.dataClass) ? 1 : _getSize(element.dataClass); // LMBCS strings are always even length int extra = String.class.equals(element.dataClass) && !element.isAscii ? length % 2 : 0; if (StringUtil.equals(name, element.name)) { // System.out.println("determined length for existing data in " + name + " is " + length); ByteBuffer data = getData().duplicate().order(ByteOrder.LITTLE_ENDIAN); byte[] replacedBytes; if (value == null) { // Then outright remove the data // We'll have to split and re-combine the underlying array replacedBytes = new byte[0]; } else { // The paths for strings and non-strings are quite different, but both result in a byte array if (String.class.equals(element.dataClass)) { String stringVal = String.valueOf(value); if (element.isAscii) { replacedBytes = stringVal.getBytes(Charset.forName("US-ASCII")); } else { replacedBytes = ODSUtils.toLMBCS(stringVal).array(); } } else { Object arrayValue = _toArrayType(value); replacedBytes = new byte[size * Array.getLength(arrayValue)]; ByteBuffer outData = ByteBuffer.wrap(replacedBytes).order(ByteOrder.LITTLE_ENDIAN); for (int i = 0; i < Array.getLength(arrayValue); i++) { Object val = Array.get(arrayValue, i); if (Byte.class.equals(element.dataClass)) { outData.put(((Number) val).byteValue()); } else if (Short.class.equals(element.dataClass)) { outData.putShort(((Number) val).shortValue()); } else if (Integer.class.equals(element.dataClass)) { outData.putInt(((Number) val).intValue()); } else if (Long.class.equals(element.dataClass)) { outData.putLong(((Number) val).longValue()); } else if (Float.class.equals(element.dataClass)) { outData.putFloat(((Number) val).floatValue()); } else if (Double.class.equals(element.dataClass)) { outData.putDouble(((Number) val).doubleValue()); } else { ByteBuffer structData = ((AbstractStruct) val).getData().duplicate(); outData.put(structData); } } } } // System.out.println("replacing with data length " + replacedBytes.length); // Check if the result size is different from the original if (replacedBytes.length == length) { // If it's the same, the job is easy data.position(data.position() + preceding); data.put(replacedBytes); } else { // Otherwise, we have to break apart the array and stitch it together // Create an array at the new total size // TODO make this only use the part of the data needed for the struct int initialPosition = data.position(); int newLength = data.capacity() - length + replacedBytes.length; byte[] newBytes = new byte[newLength]; // System.out.println("original capacity: " + data.capacity()); // System.out.println("new size: " + newLength); // Pour in the data before this element int start = data.position() + preceding; data.position(0); // System.out.println("reading data from 0 to " + (start - 1)); data.get(newBytes, 0, start); // System.arraycopy(dataArray, 0, newBytes, 0, start); // System.out.println("data's position is now " + data.position()); // Write this element's data // System.out.println("original length=" + length); // System.out.println("replacedBytes.length=" + replacedBytes.length); // System.out.println("newBytes.length=" + newBytes.length); // System.out.println("start=" + start); System.arraycopy(replacedBytes, 0, newBytes, start, replacedBytes.length); // Write any data from after this element int remaining = newBytes.length - start - replacedBytes.length; data.position(start + length); int sourceLength = data.capacity() - data.position(); if (remaining > 0) { // System.out.println("reading from data position " + data.position()); int destOffset = start + replacedBytes.length; // int sourceOffset = start + length; // System.out.println("want to write " + sourceLength + " bytes into an array of size " + newBytes.length // + " starting at " + destOffset); // System.arraycopy(dataArray, sourceOffset, newBytes, destOffset, sourceLength); data.get(newBytes, destOffset, sourceLength); } ByteBuffer newData = ByteBuffer.wrap(newBytes); newData.order(ByteOrder.LITTLE_ENDIAN).position(initialPosition); // this.setData(newBytes); this.setByteBuffer(newData, newData.position()); // If the element was defined by a field, set that field to the new length value if (field != null) { // System.out.println("setting size field to " + replacedBytes.length); if (Unsigned8.class.isAssignableFrom(field.getType())) { Unsigned8.class.getMethod("set", Short.TYPE).invoke(field.get(this), (short) replacedBytes.length); } else if (Unsigned16.class.isAssignableFrom(field.getType())) { Unsigned16.class.getMethod("set", Integer.TYPE).invoke(field.get(this), replacedBytes.length); } else if (Unsigned32.class.isAssignableFrom(field.getType())) { Unsigned32.class.getMethod("set", Long.TYPE).invoke(field.get(this), (long) replacedBytes.length); } } } // Then we're done return replacedBytes.length; } else { preceding += (size * length) + extra; } } catch (Throwable t) { throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); } } } return 0; } private static Object _toArrayType(final Object value) { Object arrayValue; if (!value.getClass().isArray()) { arrayValue = Array.newInstance(value.getClass(), 1); Array.set(arrayValue, 0, value); } else { arrayValue = value; } return arrayValue; } private static int _getSize(final Class<?> sizeClass) { if (Byte.class.equals(sizeClass)) { return 1; } else if (Short.class.equals(sizeClass)) { return 2; } else if (Integer.class.equals(sizeClass)) { return 4; } else if (Long.class.equals(sizeClass)) { return 8; } else if (Float.class.equals(sizeClass)) { return 4; } else if (Double.class.equals(sizeClass)) { return 8; } else if (Struct.class.isAssignableFrom(sizeClass)) { try { Struct example = (Struct) sizeClass.newInstance(); return example.size(); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } else { Field field = null; try { field = sizeClass.getField("SIZE"); } catch (SecurityException e1) { e1.printStackTrace(); } catch (NoSuchFieldException e1) { } if (field != null) { try { return field.getInt(sizeClass); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } throw new UnsupportedOperationException("Unknown size class " + sizeClass); } private static boolean _isPrimitive(final Class<?> clazz) { return Byte.class.equals(clazz) || Short.class.equals(clazz) || Integer.class.equals(clazz) || Long.class.equals(clazz) || Float.class.equals(clazz) || Double.class.equals(clazz); } private Object _getPrimitive(final Class<?> sizeClass, final int preceding, final boolean upgrade) { if (Byte.class.equals(sizeClass)) { byte value = getData().get(getData().position() + preceding); if (upgrade) { return (short) (value & 0xFF); } else { return value; } } else if (Short.class.equals(sizeClass)) { short value = getData().getShort(getData().position() + preceding); if (upgrade) { return value & 0xFFFF; } else { return value; } } else if (Integer.class.equals(sizeClass)) { int value = getData().getInt(getData().position() + preceding); if (upgrade) { return (long) (value & 0xFFFFFFFF); } else { return value; } } else if (Long.class.equals(sizeClass)) { // Ignore upgrade return getData().getLong(getData().position() + preceding); } else if (Float.class.equals(sizeClass)) { // Ignore upgrade return getData().getFloat(getData().position() + preceding); } else if (Double.class.equals(sizeClass)) { // Ignore upgrade return getData().getDouble(getData().position() + preceding); } return null; } // TODO check if any stdlib array-copy methods do this unboxing private static Object _toPrimitiveArray(final Object[] value, final Class<?> primitiveClass) { if (Byte.class.equals(primitiveClass)) { byte[] result = new byte[value.length]; for (int i = 0; i < value.length; i++) { result[i] = (Byte) value[i]; } return result; } else if (Short.class.equals(primitiveClass)) { short[] result = new short[value.length]; for (int i = 0; i < value.length; i++) { result[i] = (Short) value[i]; } return result; } else if (Integer.class.equals(primitiveClass)) { int[] result = new int[value.length]; for (int i = 0; i < value.length; i++) { result[i] = (Integer) value[i]; } return result; } else if (Long.class.equals(primitiveClass)) { long[] result = new long[value.length]; for (int i = 0; i < value.length; i++) { result[i] = (Long) value[i]; } return result; } else if (Float.class.equals(primitiveClass)) { float[] result = new float[value.length]; for (int i = 0; i < value.length; i++) { result[i] = (Float) value[i]; } return result; } else if (Double.class.equals(primitiveClass)) { double[] result = new double[value.length]; for (int i = 0; i < value.length; i++) { result[i] = (Double) value[i]; } return result; } return value; } // protected String buildDebugString(final String... properties) { // StringBuilder result = new StringBuilder(); // result.append("["); // result.append(getClass().getSimpleName()); // result.append(": "); // boolean addedProp = false; // for (String property : properties) { // if (addedProp) { // result.append(", "); // } else { // addedProp = true; // } // result.append(property); // result.append("="); // // try { // Method getter = getClass().getMethod("get" + property); // result.append(getter.invoke(this)); // } catch (Throwable t) { // throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); // } // } // result.append("]"); // return result.toString(); // } // protected String buildDebugString() { String currentField = null; try { StringBuilder result = new StringBuilder(); result.append("["); result.append(getClass().getSimpleName()); result.append(": "); boolean addedProp = false; for (Field field : getClass().getDeclaredFields()) { currentField = field.getName(); // TODO add array support if (Struct.Member.class.isAssignableFrom(field.getType())) { if (addedProp) { result.append(", "); } else { addedProp = true; } result.append(field.getName()); result.append("="); try { // System.out.println("getting field " + field.getName()); Method getMethod = field.getType().getDeclaredMethod("get"); result.append(getMethod.invoke(field.get(this))); } catch (Exception e) { throw new RuntimeException(e); } } } if (variableElements_.containsKey(getClass().getName())) { for (VariableElement element : variableElements_.get(getClass().getName())) { currentField = element.name; // System.out.println("getting element " + element.name); if (addedProp) { result.append(", "); } else { addedProp = true; } result.append(element.name); result.append("="); result.append(getVariableElement(element.name)); } } result.append("]"); return result.toString(); } catch (RuntimeException re) { System.err.println("RUNTIME EXCEPTION IN " + getClass().getName() + " FOR FIELD " + currentField); throw re; } } }