// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client.properties; import com.google.appinventor.client.output.OdeLog; import com.google.appinventor.client.widgets.properties.EditableProperty; import com.google.appinventor.client.widgets.properties.PropertyEditor; import com.google.appinventor.shared.properties.json.JSONObject; import com.google.appinventor.shared.properties.json.JSONValue; import java.util.Comparator; import java.util.TreeMap; import java.util.Iterator; import java.util.Map; /** * Collection of properties. * */ public class Properties<T extends Property> implements Iterable<T> { // We define our own special comparator here. It turns out that // properties are displayed in the properties panel based on the order // they are pulled out of the map. When we used a hashmap, the height // and width properties appeared together (lucky I guess). When // we replaced it with a TreeMap, properties now appear in the panel // in sort order (based on their English names). This separated // the heigth and width proprites. This comparator arranges for // "Width" to be sorted immediately after "Height." So properties // are now displayed in sort order, with this notable exception, the // width property now appears immediately after the Height property // which is what people have grown used to (and seems correct). private class Comparer implements Comparator<String> { public int compare(String a, String b) { if (a.equals("Width")) a = "Heightz"; if (b.equals("Width")) b = "Heightz"; return a.compareTo(b); } } // Maps property names to properties private final Map<String, T> propertiesMap; /** * Creates a new properties instance. */ public Properties() { // Note: We used to use a HashMap here. However when we iterate over its // contents we get the keys in an arbitrary order based on the hash // implementation. We have had at least one case where an update of // the GWT library changed the order which triggered unanticipated // bugs. By using a TreeMap we will get the keys in a consistent order propertiesMap = new TreeMap<String, T>(new Comparer()); } /** * Changes the properties from an encoded string of properties. * * @param propertiesObject JSON encoded properties */ public void changeProperties(JSONObject propertiesObject) { Map<String, JSONValue> properties = propertiesObject.getProperties(); for (String name : properties.keySet()) { changePropertyValue(name, properties.get(name).asString().getString()); } } /** * Encodes properties whose value is different from their default value or * as a JSON string. * * @return encoded properties */ public final String encodeAsJsonString() { return '{' + encodeAsPairs(false) + '}'; } /** * Encodes all properties as a JSON string. * * @return encoded properties */ public final String encodeAllAsJsonString() { return '{' + encodeAllAsPairs() + '}'; } /** * Encodes properties whose value is different from their default value * as JSON pairs. * * @return encoded properties */ public String encodeAsPairs(boolean forYail) { return encode(forYail, false); } /** * Encodes all properties as JSON pairs. * * @return encoded properties */ public String encodeAllAsPairs() { return encode(true, true); } /** * Encodes properties as JSON pairs. * * @param all indicates whether all persistable properties should be encoded * or only those that have value different from their default * value * @return encoded property pairs */ protected String encode(boolean forYail, boolean all) { StringBuilder sb = new StringBuilder(); sb.append(getPrefix()); String separator = ""; for (Property property : this) { // Don't encode non-persistable properties or properties being assigned their default value // unless encoding for all properties was explicitly requested if ((property.isPersisted() || (property.isYail() && forYail)) && (all || !property.getDefaultValue().equals(property.getValue()))) { sb.append(separator); separator = ","; property.encode(sb); } } sb.append(getSuffix()); return sb.toString(); } /** * Optional prefix to add to encoded properties. * * @return prefix */ protected String getPrefix() { return ""; } /** * Optional suffix to add to encoded properties. * * @return suffix */ protected String getSuffix() { return ""; } /** * Adds a new property to the collection. * A property of the same name may already exist. * * @param property property to be added */ protected void addProperty(T property) { T oldProperty = propertiesMap.put(property.getName(), property); if (oldProperty != null) { propertiesMap.put(property.getName(), oldProperty); // restore state throw new IllegalStateException("property already exists: " + property.getName()); } } /** * Removes a property from the collection * The property may not exist in the collection * * @param propertyName name of the property to be removed */ protected void removeProperty(String propertyName) { if (propertiesMap.containsKey(propertyName)) { propertiesMap.remove(propertyName); } } /** * Deletes all properties. */ public final void deleteAllProperties() { propertiesMap.clear(); } /** * Changes the value of an existing property. * * @param name property name * @param value new property value * @return true if the property exists and was changed, false otherwise */ public final boolean changePropertyValue(String name, String value) { try { getExistingProperty(name).setValue(value); return true; } catch (IllegalStateException e) { OdeLog.wlog(e.toString()); return false; } } public final boolean hasProperty(String name) { return propertiesMap.containsKey(name); } /** * Returns the value of an existing property. * * @param name property name * @return value of the property * @throws IllegalStateException if no such property exists */ public final String getPropertyValue(String name) { return getExistingProperty(name).getValue(); } /** * Returns an Iterator over the properties. * * @return Property iterator */ @Override public final Iterator<T> iterator() { return propertiesMap.values().iterator(); } /** * Returns the property for the given name, * or {@code null} if no such property has been defined. * * @param name property name */ public final T getProperty(String name) { return propertiesMap.get(name); } /** * Returns the existing property for the given name. * * @throws IllegalStateException if no such property exists */ public T getExistingProperty(String name) { T property = getProperty(name); if (property == null) { throw new IllegalStateException("no such property: " + name); } return property; } /** * Resets the values of all properties to their default values. */ public final void resetProperties() { for (Property property : this) { property.resetToDefault(); } } }