/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.properties;
import com.codename1.io.Externalizable;
import com.codename1.io.JSONParser;
import com.codename1.io.Log;
import com.codename1.io.Storage;
import com.codename1.io.Util;
import com.codename1.processing.Result;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Maps the properties that are in a class/object and provides access to them so tools such as ORM
* can implicitly access them for us. This class also holds the class level meta-data for a specific property
* or class. It also provides utility level tools e.g. toString implementation etc.
*
* @author Shai Almog
*/
public class PropertyIndex implements Iterable<PropertyBase> {
private final PropertyBase[] properties;
private static HashMap<String, HashMap<String, Object>> metadata = new HashMap<String, HashMap<String, Object>>();
PropertyBusinessObject parent;
private final String name;
/**
* The constructor is essential for a proper property business object
*
* @param parent the parent object instance
* @param name the name of the parent class
* @param properties the list of properties in the object
*/
public PropertyIndex(PropertyBusinessObject parent, String name, PropertyBase... properties) {
this.properties = properties;
this.parent = parent;
this.name = name;
for(PropertyBase p : properties) {
p.parent = this;
}
}
/**
* The name of the parent business object
* @return a unique name for the parent
*/
public String getName() {
return name;
}
/**
* Returns a property by its name
* @param name the name of the property (case sensitive)
* @return the property or null
*/
public PropertyBase get(String name) {
for(PropertyBase p : properties) {
if(p.getName().equals(name)) {
return p;
}
}
return null;
}
/**
* Returns a property by its name regardless of case sensitivity for the name
* @param name the name of the property (case insensitive)
* @return the property or null
*/
public PropertyBase getIgnoreCase(String name) {
for(PropertyBase p : properties) {
if(p.getName().equalsIgnoreCase(name)) {
return p;
}
}
return null;
}
/**
* Allows us to get an individual property within the object instance
* @param i the index of the property
* @return the property instance
*/
public PropertyBase get(int i) {
return properties[i];
}
/**
* The number of properties in the class
* @return number of properties in the class
*/
public int getSize() {
return properties.length;
}
/**
* Allows us to traverse the properties with a for-each statement
* @return an iterator instance
*/
public Iterator<PropertyBase> iterator() {
return new Iterator<PropertyBase>() {
int off = 0;
public boolean hasNext() {
return off < properties.length;
}
public void remove() {
}
public PropertyBase next() {
int i = off;
off++;
return properties[i];
}
};
}
private HashMap<String, Object> getProps() {
HashMap<String,Object> m = metadata.get(parent.getClass().getName());
if(m == null) {
m = new HashMap<String, Object>();
metadata.put(parent.getClass().getName(), m);
}
return m;
}
/**
* Allows us to fetch class meta data not to be confused with standard properties
* @param meta the meta data unique name
* @return the object instance
*/
public Object getMetaDataOfClass(String meta) {
return getProps().get(meta);
}
/**
* Sets class specific metadata
*
* @param meta the name of the meta data
* @param o object value for the meta data
*/
public void putMetaDataOfClass(String meta, Object o) {
if(o == null) {
getProps().remove(meta);
} else {
getProps().put(meta, o);
}
}
/**
* Returns a user readable printout of the property values which is useful for debugging
* @return user readable printout of the property values which is useful for debugging
*/
public String toString() {
StringBuilder b = new StringBuilder(name);
b.append(" : {\n");
for(PropertyBase p : this) {
b.append(p.getName());
b.append(" = ");
b.append(p.toString());
b.append("\n");
}
b.append("}");
return b.toString();
}
/**
* This is useful for JSON parsing, it allows converting JSON map data to objects
* @param m the map
*/
public void populateFromMap(Map<String, Object> m) {
populateFromMap(m, null);
}
private Object listParse(List l, Class<? extends PropertyBusinessObject>recursiveType) throws InstantiationException, IllegalAccessException {
ArrayList al = new ArrayList();
for(Object o : l) {
if(o instanceof Map) {
PropertyBusinessObject po = (PropertyBusinessObject)recursiveType.newInstance();
po.getPropertyIndex().populateFromMap((Map<String, Object>)o, recursiveType);
al.add(po);
continue;
}
if(o instanceof List) {
al.add(listParse((List)o,recursiveType));
continue;
}
al.add(o);
}
return al;
}
/**
* This is useful for JSON parsing, it allows converting JSON map data to objects
* @param m the map
* @param recursiveType when running into map types we create this object type
*/
public void populateFromMap(Map<String, Object> m, Class<? extends PropertyBusinessObject>recursiveType) {
try {
for(PropertyBase p : this) {
Object val = m.get(p.getName());
if(val != null) {
if(val instanceof List) {
if(p instanceof ListProperty) {
if(recursiveType != null) {
if(((ListProperty)p) != null) {
((ListProperty)p).clear();
}
for(Object e : (Collection)val) {
if(e instanceof Map) {
Class eType = ((ListProperty) p).getGenericType();
// maybe don't use recursiveType here anymore???
// elementType is usually sufficient...
Class type = (eType == null)? recursiveType : eType;
PropertyBusinessObject po = (PropertyBusinessObject)type.newInstance();
po.getPropertyIndex().populateFromMap((Map<String, Object>)e, type);
((ListProperty)p).add(po);
continue;
}
if(e instanceof List) {
((ListProperty)p).add(listParse((List)e, recursiveType));
continue;
}
((ListProperty)p).add(e);
}
} else {
((ListProperty)p).setList((Collection)val);
}
}
continue;
}
if(val instanceof Map) {
if(p instanceof MapProperty) {
((MapProperty)p).clear();
for(Object k : ((Map)val).keySet()) {
Object value = ((Map)val).get(k);
if(value instanceof Map) {
PropertyBusinessObject po = (PropertyBusinessObject)p.get();
po.getPropertyIndex().populateFromMap((Map<String, Object>)value, recursiveType);
((MapProperty)p).set(k, po);
continue;
}
if(value instanceof List) {
((MapProperty)p).set(k, listParse((List)value, recursiveType));
continue;
}
((MapProperty)p).set(k, value);
}
continue;
} else {
if(p.get() instanceof PropertyBusinessObject) {
PropertyBusinessObject po = (PropertyBusinessObject)p.get();
po.getPropertyIndex().populateFromMap((Map<String, Object>)val, recursiveType);
} else {
if(recursiveType != null) {
PropertyBusinessObject po = (PropertyBusinessObject)recursiveType.newInstance();
po.getPropertyIndex().populateFromMap((Map<String, Object>)val, recursiveType);
p.setImpl(po);
}
}
}
continue;
}
if(p instanceof IntProperty) {
p.setImpl(Util.toIntValue(val));
continue;
}
if(p instanceof LongProperty) {
p.setImpl(Util.toLongValue(val));
continue;
}
if(p instanceof FloatProperty) {
p.setImpl(Util.toFloatValue(val));
continue;
}
if(p instanceof DoubleProperty) {
p.setImpl(Util.toDoubleValue(val));
continue;
}
p.setImpl(val);
}
}
} catch(InstantiationException err) {
Log.e(err);
throw new RuntimeException("Can't create instanceof class: " + err);
} catch(IllegalAccessException err) {
Log.e(err);
throw new RuntimeException("Can't create instanceof class: " + err);
}
}
/**
* This is useful in converting a property object to JSON
* @return a map representation of the properties
*/
public Map<String, Object> toMapRepresentation() {
return toMapRepresentationImpl("mapExclude");
}
/**
* This is useful in converting a property object to JSON
* @return a map representation of the properties
*/
private Map<String, Object> toMapRepresentationImpl(String excludeFlag) {
HashMap<String, Object> m = new HashMap<String, Object>();
for(PropertyBase p : this) {
if(p.getClientProperty(excludeFlag) != null) {
continue;
}
if(p instanceof MapProperty) {
MapProperty pp = (MapProperty)p;
m.put(p.getName(), pp.asExplodedMap());
continue;
}
if(p instanceof ListProperty) {
ListProperty pp = (ListProperty)p;
m.put(p.getName(), pp.asExplodedList());
continue;
}
if(p instanceof Property) {
Property pp = (Property)p;
if(pp.get() != null) {
m.put(p.getName(), pp.get());
}
}
}
return m;
}
/**
* Converts the object to a JSON representation
* @return a JSON String
*/
public String toJSON() {
return Result.fromContent(toMapRepresentationImpl("jsonExclude")).toString();
}
/**
* Writes the JSON string to storage, it's a shortcut for writing/generating the JSON
* @param name the name of the storage file
*/
public void storeJSON(String name) {
try {
OutputStream os = Storage.getInstance().createOutputStream(name);
os.write(toJSON().getBytes("UTF-8"));
os.close();
} catch(IOException err) {
Log.e(err);
throw new RuntimeException(err.toString());
}
}
/**
* Loads JSON for the object from storage with the given name
* @param name the name of the storage
*/
public void loadJSON(String name) {
try {
InputStream is = Storage.getInstance().createInputStream(name);
JSONParser jp = new JSONParser();
populateFromMap(jp.parseJSON(new InputStreamReader(is, "UTF-8")), parent.getClass());
} catch(IOException err) {
Log.e(err);
throw new RuntimeException(err.toString());
}
}
/**
* Returns true if the given object equals the property index
* @param o the object
* @return true if equals
*/
public boolean equals(Object o) {
if(o instanceof PropertyIndex) {
PropertyIndex other = (PropertyIndex)o;
if(parent == other.parent) {
return true;
}
if(parent.getClass() != other.parent.getClass()) {
return false;
}
if(properties.length == other.properties.length) {
for(int iter = 0 ; iter < properties.length ; iter++) {
if(!properties[iter].equals(other.properties[iter])) {
return false;
}
}
return true;
}
}
return false;
}
/**
* The hashcode of the object
* @return a composite of the hashcodes of the properties
*/
@Override
public int hashCode() {
int value = 0;
for(int iter = 0 ; iter < properties.length ; iter++) {
if(properties[iter] instanceof Property) {
Object v = ((Property)properties[iter]).get();
if(v != null) {
int b = v.hashCode();
value = 31 * value + (int) (b ^ (b >>> 32));
}
}
}
return value;
}
/**
* Allows us to exclude a specific property from the toJSON process
* @param pb the property
* @param exclude true to exclude and false to reinclude
*/
public void setExcludeFromJSON(PropertyBase pb, boolean exclude) {
if(exclude) {
pb.putClientProperty("jsonExclude", Boolean.TRUE);
} else {
pb.putClientProperty("jsonExclude", null);
}
}
/**
* Indicates whether the given property is excluded from the {@link #toMapRepresentation()}
* method output
* @param pb the property
* @return true if the property is excluded and false otherwise
*/
public boolean isExcludeFromMap(PropertyBase pb) {
return pb.getClientProperty("mapExclude") != null;
}
/**
* Allows us to exclude a specific property from the {@link #toMapRepresentation()} process
* @param pb the property
* @param exclude true to exclude and false to reinclude
*/
public void setExcludeFromMap(PropertyBase pb, boolean exclude) {
if(exclude) {
pb.putClientProperty("mapExclude", Boolean.TRUE);
} else {
pb.putClientProperty("jsonExclude", null);
}
}
/**
* Indicates whether the given property is excluded from the {@link #toJSON()} method output
* @param pb the property
* @return true if the property is excluded and false otherwise
*/
public boolean isExcludeFromJSON(PropertyBase pb) {
return pb.getClientProperty("jsonExclude") != null;
}
/**
* Invoking this method will allow a property object to be serialized seamlessly
*/
public void registerExternalizable() {
Util.register(getName(), parent.getClass());
}
/**
* Returns an externalizable object for serialization of this business object, unlike regular
* externalizables this implementation is robust to changes, additions and removals of
* properties
* @return an externalizable instance
*/
public Externalizable asExternalizable() {
return new Externalizable() {
public int getVersion() {
return 1;
}
public void externalize(DataOutputStream out) throws IOException {
out.writeInt(getSize());
for(PropertyBase b : PropertyIndex.this) {
out.writeUTF(b.getName());
if(b instanceof ListProperty) {
out.writeByte(2);
Util.writeObject(((ListProperty)b).asList(), out);
continue;
}
if(b instanceof MapProperty) {
out.writeByte(3);
Util.writeObject(((MapProperty)b).asMap(), out);
continue;
}
if(b instanceof Property) {
out.writeByte(1);
Util.writeObject(((Property)b).get(), out);
continue;
}
}
}
public void internalize(int version, DataInputStream in) throws IOException {
int size = in.readInt();
for(int iter = 0 ; iter < size ; iter++) {
String pname = in.readUTF();
int type = in.readByte();
Object data = Util.readObject(in);
PropertyBase pb = get(pname);
switch(type) {
case 1: // Property
if(pb instanceof Property) {
((Property)pb).set(data);
}
break;
case 2: // ListProperty
if(pb instanceof ListProperty) {
((ListProperty)pb).setList((List)data);
}
break;
case 3: // MapProperty
if(pb instanceof MapProperty) {
((MapProperty)pb).setMap((Map)data);
}
break;
}
}
}
public String getObjectId() {
return getName();
}
};
}
}