blob: 5fd3cd54a857458ba17aa6c547eff3672f1fba54 [file] [log] [blame]
/*******************************************************************************
* 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;
}
/**
* Reset to the given next index.
* @param nextIndex the next index to do a read at.
*
* @since 1.1.0
*/
public void reset(int nextIndex) {
if (nextIndex<=length)
next = nextIndex;
else
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", null); //$NON-NLS-1$
Method getLogger = biPluginClass.getMethod("getLogger", null); //$NON-NLS-1$
Method log = getLogger.getReturnType().getMethod("log", new Class[] {Throwable.class, Level.class}); //$NON-NLS-1$
Object biPlugin = getPlugin.invoke(null, null);
Object logger = getLogger.invoke(biPlugin, 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;
}
}