/******************************************************************************* * Copyright (c) 2005, 2006 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ /* */ package org.eclipse.jem.internal.beaninfo.common; import java.io.*; import java.lang.reflect.*; import java.util.Arrays; import java.util.logging.Level; import java.util.regex.Pattern; import org.eclipse.jem.internal.proxy.common.MapTypes; /** * This is the value for a FeatureAttribute. It wrappers the true java object. * Use the getObject method to get the java value. * <p> * We can only represent Strings, primitives, and arrays. (Primitives will converted * to their wrapper class (e.g. Long), and byte, short, and int will move up to Long, * and float will move up to Double). And any kind of valid array on the java BeanInfo side * will be converted to an Object array on the IDE side. We don't have the capability to allow more complex objects * because the IDE may not have the necessary classes available to it that * the BeanInfo may of had available to it. Invalid objects will be represented * by the singleton instance of {@link org.eclipse.jem.internal.beaninfo.common.InvalidObject}. * <p> * <b>Note:</b> * Class objects that are values of Feature attributes on the java BeanInfo side will be * converted to simple strings containing the classname when moved to the client (IDE) side. * That is because the classes probably will not be available on the IDE side, but can be * used to reconstruct the class when used back on the java vm side. * @since 1.1.0 */ public class FeatureAttributeValue implements Serializable { private transient Object value; private transient Object internalValue; private boolean implicitValue; private static final long serialVersionUID = 1105717634844L; /** * Create the value with the given init string. * <p> * This is not meant to be used by clients. * @param initString * * @since 1.1.0 */ public FeatureAttributeValue(String initString) { // Use the init string to create the value. This is our // own short-hand for this. if (initString.startsWith(IMPLICIT)) { setImplicitValue(true); initString = initString.substring(IMPLICIT.length()); } value = parseString(initString); } /** * This is used when customer wants to fluff one up. * * * @since 1.1.0 */ public FeatureAttributeValue() { } /** * @return Returns the value. * * @since 1.1.0 */ public Object getValue() { return value; } /** * Set a value. * @param value The value to set. * @since 1.1.0 */ public void setValue(Object value) { this.value = value; this.setInternalValue(null); } /** * Set the internal value. * @param internalValue The internalValue to set. * * @since 1.1.0 */ public void setInternalValue(Object internalValue) { this.internalValue = internalValue; } /** * This is the internal value. It is the <code>value</code> massaged into an easier to use form * in the IDE. It will not be serialized out. It will not be reconstructed from an init string. * <p> * It does not need to be used. It will be cleared if * a new value is set. For example, if the value is a complicated array (because you can't have * special classes in the attribute value on the BeanInfo side) the first usage of this value can * be translated into an easier form to use, such as a map. * * @return Returns the internalValue. * * @since 1.1.0 */ public Object getInternalValue() { return internalValue; } /* (non-Javadoc) * @see java.lang.Object#toString() */ public String toString() { if (value == null) return super.toString(); StringBuffer out = new StringBuffer(100); if (isImplicitValue()) out.append(IMPLICIT); makeString(value, out); return out.toString(); } /** * Helper method to take the object and turn it into the * string form that is required for EMF serialization. * <p> * This is used internally. It can be used for development * purposes by clients, but they would not have any real * runtime need for this. * <p> * Output format would be (there won't be any newlines in the actual string) * <pre> * String: "zxvzxv" * Number: number * Boolean: true or false * Character: 'c' * null: null * * Array: (all arrays will be turned into Object[]) * [dim]{e1, e2} * [dim1][dim2]{[dim1a]{e1, e2}, [dim2a]{e3, e4}} * where en are objects that follow the pattern for single output above. * * Any invalid object (i.e. not one of the ones we handle) will be: * INV * * Arrays of invalid types (not Object, String, Number, Boolean, Character, * or primitives) will be marked as INV. * </pre> * @param value * @return serialized form as a string. * * @since 1.1.0 */ public static String makeString(Object value) { StringBuffer out = new StringBuffer(100); makeString(value, out); return out.toString(); } private static final Pattern QUOTE = Pattern.compile("\""); // Pattern for searching for double-quote. Make it static so don't waste time compiling each time. //$NON-NLS-1$ private static final String NULL = "null"; // Output string for null //$NON-NLS-1$ private static final String INVALID = "INV"; // Invalid object flag. //$NON-NLS-1$ private static final String IMPLICIT = "implicit,"; // Implicit flag. //$NON-NLS-1$ /* * Used for recursive building of the string. */ private static void makeString(Object value, StringBuffer out) { if (value == null) out.append(NULL); else if (value instanceof String || value instanceof Class) { // String: "string" or "string\"stringend" if str included a double-quote. out.append('"'); // If class, turn value into the classname. String str = value instanceof String ? (String) value : ((Class) value).getName(); if (str.indexOf('"') != -1) { // Replace double-quote with escape double-quote so we can distinquish it from the terminating double-quote. out.append(QUOTE.matcher(str).replaceAll("\\\\\"")); // Don't know why we need the back-slash to be doubled for replaceall, but it doesn't work otherwise. //$NON-NLS-1$ } else out.append(str); out.append('\"'); } else if (value instanceof Number) { // Will go out as either a integer number or a floating point number. // When read back in it will be either a Long or a Double. out.append(value); } else if (value instanceof Boolean) { // It will go out as either true or false. out.append(value); } else if (value instanceof Character) { // Character: 'c' or '\'' if char was a quote. out.append('\''); Character c = (Character) value; if (c.charValue() != '\'') out.append(c.charValue()); else out.append("\\'"); //$NON-NLS-1$ out.append('\''); } else if (value.getClass().isArray()) { // Handle array format. Class type = value.getClass(); // See if final type is a valid type. Class ft = type.getComponentType(); int dims = 1; while (ft.isArray()) { dims++; ft = ft.getComponentType(); } if (ft == Object.class || ft == String.class || ft == Boolean.class || ft == Character.class || ft.isPrimitive() || Number.class.isAssignableFrom(ft)) { // [length][][] {....} out.append('['); int length = Array.getLength(value); out.append(length); out.append(']'); while(--dims > 0) { out.append("[]"); //$NON-NLS-1$ } out.append('{'); for (int i=0; i < length; i++) { if (i != 0) out.append(','); makeString(Array.get(value, i), out); } out.append('}'); } else out.append(INVALID); // Any other kind of array is invalid. } else { out.append(INVALID); } } /** * Helper method to take the string input from EMF serialization and turn it * into an Object. * <p> * This is used internally. It can be used for development * purposes by clients, but they would not have any real * runtime need for this. * <p> * The object will be an object, null, or an Object array. Any value * that is invalid will be set to the {@link InvalidObject#INSTANCE} static * instance. * * @param input * @return object decoded from the input. * * @see #makeString(Object) * @since 1.1.0 */ public static Object parseString(String input) { return parseString(new StringParser(input)); } private static class StringParser { private int next=0; private int length; private String input; public StringParser(String input) { this.input = input; this.length = input.length(); } public String toString() { return "StringParser: \""+input+'"'; //$NON-NLS-1$ } public void skipWhitespace() { while(next < length) { if (!Character.isWhitespace(input.charAt(next++))) { next--; // Put it back as not yet read since it is not whitespace. break; } } } /** * Return the next index * @return * * @since 1.1.0 */ public int nextIndex() { return next; } /** * Get the length of the input * @return input length * * @since 1.1.0 */ public int getLength() { return length; } /** * Read the current character and go to next. * @return current character * * @since 1.1.0 */ public char read() { return next<length ? input.charAt(next++) : 0; } /** * Backup the parser one character. * * * @since 1.1.0 */ public void backup() { if (--next < 0) next = 0; } /** * Peek at the char at the next index, but don't increment afterwards. * @return * * @since 1.1.0 */ public char peek() { return next<length ? input.charAt(next) : 0; } /** * Have we read the last char. * @return <code>true</code> if read last char. * * @since 1.1.0 */ public boolean atEnd() { return next>=length; } /** * Skip the next number of chars. * @param skip number of chars to skip. * * @since 1.1.0 */ public void skip(int skip) { if ((next+=skip) > length) next = length; } /** * Return the string input. * @return the string input * * @since 1.1.0 */ public String getInput() { return input; } } /* * Starting a parse for an object at the given index. * Return the parsed object or InvalidObject if no * object or if there was an error parsing. */ private static Object parseString(StringParser parser) { parser.skipWhitespace(); if (!parser.atEnd()) { char c = parser.read(); switch (c) { case '"': // Start of a quoted string. Scan for closing quote, ignoring escaped quotes. int start = parser.nextIndex(); // Index of first char after '"' char[] dequoted = null; // Used if there is an escaped quote. That is the only thing we support escape on, quotes. int dequoteIndex = 0; while (!parser.atEnd()) { char cc = parser.read(); if (cc == '"') { // If we didn't dequote, then just do substring. if (dequoted == null) return parser.getInput().substring(start, parser.nextIndex()-1); // next is char after '"', so end of string index is index of '"' else { // We have a dequoted string. So turn into a string. // Gather the last group int endNdx = parser.nextIndex()-1; parser.getInput().getChars(start, endNdx, dequoted, dequoteIndex); dequoteIndex+= (endNdx-start); return new String(dequoted, 0, dequoteIndex); } } else if (cc == '\\') { // We had an escape, see if next is a quote. If it is we need to strip out the '\'. if (parser.peek() == '"') { if (dequoted == null) { dequoted = new char[parser.getLength()]; } int endNdx = parser.nextIndex()-1; parser.getInput().getChars(start, endNdx, dequoted, dequoteIndex); // Get up to, but not including '\' dequoteIndex+= (endNdx-start); // Now also add in the escaped quote. dequoted[dequoteIndex++] = parser.read(); start = parser.nextIndex(); // Next group is from next index. } } } break; // If we got here, it is invalid. case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': // Possible number. // Scan to next non-digit, or not part of valid number. boolean numberComplete = false; boolean floatType = false; boolean foundE = false; boolean foundESign = false; start = parser.nextIndex()-1; // We want to include the sign or first digit in the number. while (!parser.atEnd() && !numberComplete) { char cc = parser.read(); switch (cc) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; // This is good, go on. case '.': if (floatType) return InvalidObject.INSTANCE; // We already found a '.', two are invalid. floatType = true; break; case 'e': case 'E': if (foundE) return InvalidObject.INSTANCE; // We already found a 'e', two are invalid. foundE = true; floatType = true; // An 'e' makes it a float, if not already. break; case '+': case '-': if (!foundE || foundESign) return InvalidObject.INSTANCE; // A +/- with no 'e' first is invalid. Or more than one sign. foundESign = true; break; default: // Anything else is end of number. parser.backup(); // Back it up so that next parse will start with this char. numberComplete = true; // So we stop scanning break; } } try { if (!floatType) return Long.valueOf(parser.getInput().substring(start, parser.nextIndex())); else return Double.valueOf(parser.getInput().substring(start, parser.nextIndex())); } catch (NumberFormatException e) { } break; // If we got here, it is invalid. case 't': case 'T': case 'f': case 'F': // Possible boolean. if (parser.getInput().regionMatches(true, parser.nextIndex()-1, "true", 0, 4)) { //$NON-NLS-1$ parser.skip(3); // Skip over rest of string. return Boolean.TRUE; } else if (parser.getInput().regionMatches(true, parser.nextIndex()-1, "false", 0, 5)) { //$NON-NLS-1$ parser.skip(4); // Skip over rest of string. return Boolean.FALSE; } break; // If we got here, it is invalid. case '\'': // Possible character char cc = parser.read(); // We really only support '\\' and '\'' anything else will be treated as ignore '\' because we don't know handle full escapes. if (cc == '\\') cc = parser.read(); // Get what's after it. else if (cc == '\'') break; // '' is invalid. if (parser.peek() == '\'') { // So next char after "character" is is a quote. This is good. parser.read(); // Now consume the quote return new Character(cc); } break; // If we got here, it is invalid. case 'n': // Possible null. if (parser.getInput().regionMatches(parser.nextIndex()-1, "null", 0, 4)) { //$NON-NLS-1$ parser.skip(3); // Skip over rest of string. return null; } break; // If we got here, it is invalid. case 'I': // Possible invalid value. if (parser.getInput().regionMatches(parser.nextIndex()-1, INVALID, 0, INVALID.length())) { parser.skip(INVALID.length()-1); // Skip over rest of string. return InvalidObject.INSTANCE; } break; // If we got here, it is invalid. case '[': // Possible array. // The next field should be a number, so we'll use parseString to get the number. Object size = parseString(parser); if (size instanceof Long) { parser.skipWhitespace(); cc = parser.read(); // Se if next is ']' if (cc == ']') { // Good, well-formed first dimension int dim = 1; boolean valid = true; // See if there are more of just "[]". the number of them is the dim. while (true) { parser.skipWhitespace(); cc = parser.read(); if (cc == '[') { parser.skipWhitespace(); cc = parser.read(); if (cc == ']') dim++; else { // This is invalid. valid = false; parser.backup(); break; // No more dims. } } else { parser.backup(); break; // No more dims. } } if (valid) { parser.skipWhitespace(); cc = parser.read(); if (cc == '{') { // Good, we're at the start of the initialization code. int[] dims = new int[dim]; int len = ((Long) size).intValue(); dims[0] = len; Object array = Array.newInstance(Object.class, dims); Arrays.fill((Object[]) array, null); // Because newInstance used above fills the array created with empty arrays when a dim>1. // Now we start filling it in. Object invSetting = null; // What we will use for the invalid setting. If this is a multidim, this needs to be an array. Will not create it until needed. Object entry = parseString(parser); // Get the first entry Class compType = array.getClass().getComponentType(); int i = -1; while (true) { if (++i < len) { if (compType.isInstance(entry)) { // Good, it can be assigned. Array.set(array, i, entry); } else { // Bad. Need to set invalid. if (invSetting == null) { // We haven't created it yet. if (dim == 1) invSetting = InvalidObject.INSTANCE; // Great, one dimensional, we can use invalid directly else { // Multi-dim. Need to create a valid array that we can set. int[] invDims = new int[dim - 1]; Arrays.fill(invDims, 1); // Length one all of the way so that the final component can be invalid object invSetting = Array.newInstance(Object.class, invDims); Object finalEntry = invSetting; // Final array (with component type of just Object). Start with the full array and work down. for (int j = invDims.length - 1; j > 0; j--) { finalEntry = Array.get(finalEntry, 0); } Array.set(finalEntry, 0, InvalidObject.INSTANCE); } } Array.set(array, i, invSetting); } } parser.skipWhitespace(); cc = parser.read(); if (cc == ',') { // Good, get next entry = parseString(parser); } else if (cc == '}') { // Good, reached the end. break; } else { if (!parser.atEnd()) { parser.backup(); entry = parseString(parser); // Technically this should be invalid, but we'll let a whitespace also denote next entry. } else { // It's really screwed up. The string just ended. Log it. Exception e = new IllegalStateException(parser.toString()); try { // See if Beaninfo plugin is available (we are running under eclipse). If so, use it, else just print to error. // We may be in the remote vm and so it won't be available. Class biPluginClass = Class.forName("org.eclipse.jem.internal.beaninfo.core.BeaninfoPlugin"); //$NON-NLS-1$ Method getPlugin = biPluginClass.getMethod("getPlugin", (Class[]) null); //$NON-NLS-1$ Method getLogger = biPluginClass.getMethod("getLogger", (Class[]) null); //$NON-NLS-1$ Method log = getLogger.getReturnType().getMethod("log", new Class[] {Throwable.class, Level.class}); //$NON-NLS-1$ Object biPlugin = getPlugin.invoke(null, (Object[]) null); Object logger = getLogger.invoke(biPlugin, (Object[]) null); log.invoke(logger, new Object[] {e, Level.WARNING}); return InvalidObject.INSTANCE; } catch (SecurityException e1) { } catch (IllegalArgumentException e1) { } catch (ClassNotFoundException e1) { } catch (NoSuchMethodException e1) { } catch (IllegalAccessException e1) { } catch (InvocationTargetException e1) { } catch (NullPointerException e1) { } e.printStackTrace(); // Not in eclipse, so just print stack trace. return InvalidObject.INSTANCE; } } } return array; } } } } break; // If we got here, it is invalid. } } return InvalidObject.INSTANCE; } private void writeObject(ObjectOutputStream out) throws IOException { // Write out any hidden stuff out.defaultWriteObject(); writeObject(value, out); } private void writeObject(Object value, ObjectOutputStream out) throws IOException { if (value == null) out.writeObject(value); else { if (value instanceof Class) out.writeObject(((Class) value).getName()); else if (!value.getClass().isArray()) { if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Character) out.writeObject(value); else out.writeObject(InvalidObject.INSTANCE); } else { // Array is tricky. See if it is one we can handle, if not then invalid. // To indicate array, we will first write out the Class of the Component type of the array (it will // be converted to be Object or Object[]...). // This will be the clue that an array is coming. Class values will never // be returned, so that is how we can tell it is an array. // Note: The reason we are using the component type (converted to Object) is because to reconstruct on the other side we need // to use the component type plus length of the array's first dimension. // // We can not just serialize the array in the normal way because it may contain invalid values, and we need to // handle that. Also, if it wasn't an Object array, we need to turn it into an object array. We need consistency // in that it should always be an Object array. // So output format will be: // Class(component type) // int(size of first dimension) // Object(value of first entry) - Actually use out writeObject() format to allow nesting of arrays. // Object(value of second entry) // ... up to size of dimension. Class type = value.getClass(); // See if final type is a valid type. Class ft = type.getComponentType(); int dims = 1; while (ft.isArray()) { dims++; ft = ft.getComponentType(); } if (ft == Object.class || ft == String.class || ft == Boolean.class || ft == Character.class || ft.isPrimitive() || ft == Class.class || Number.class.isAssignableFrom(ft)) { String jniType = dims == 1 ? "java.lang.Object" : MapTypes.getJNITypeName("java.lang.Object", dims-1); //$NON-NLS-1$ //$NON-NLS-2$ try { Class componentType = Class.forName(jniType); out.writeObject(componentType); int length = Array.getLength(value); out.writeInt(length); for (int i = 0; i < length; i++) { writeObject(Array.get(value, i), out); } } catch (ClassNotFoundException e) { // This should never happen. Object arrays are always available. } } else out.writeObject(InvalidObject.INSTANCE); } } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // Read in any hidden stuff in.defaultReadObject(); value = readActualObject(in); } private Object readActualObject(ObjectInputStream in) throws IOException, ClassNotFoundException { Object val = in.readObject(); if (val instanceof Class) { // It must be an array. Only Class objects that come in are Arrays of Object. int length = in.readInt(); Object array = Array.newInstance((Class) val, length); for (int i = 0; i < length; i++) { Array.set(array, i, readActualObject(in)); } return array; } else return val; // It is the value itself. } /** * Is this FeatureAttributeValue an implicit value, i.e. one that came from * BeanInfo Introspection and not from an override file. * * @return Returns the implicitValue. * * @since 1.2.0 */ public boolean isImplicitValue() { return implicitValue; } /** * Set the implicit value flag. * @param implicitValue The implicitValue to set. * * @see #isImplicitValue() * @since 1.2.0 */ public void setImplicitValue(boolean implicitValue) { this.implicitValue = implicitValue; } }