1 /* 2 * Copyright 2012 AndroidPlot.com 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.androidplot.util; 18 19 import android.content.Context; 20 import android.content.res.XmlResourceParser; 21 import android.graphics.Color; 22 import android.util.Log; 23 import android.util.TypedValue; 24 import org.xmlpull.v1.XmlPullParserException; 25 26 import java.io.IOException; 27 import java.lang.reflect.InvocationTargetException; 28 import java.lang.reflect.Method; 29 import java.lang.reflect.Type; 30 import java.util.HashMap; 31 32 /** 33 * Utility class for "configuring" objects via XML config files. Supports the following field types: 34 * String 35 * Enum 36 * int 37 * float 38 * boolean 39 * <p/> 40 * Config files should be stored in /res/xml. Given the XML configuration /res/xml/myConfig.xml, one can apply the 41 * configuration to an Object instance as follows: 42 * <p/> 43 * MyObject obj = new MyObject(); 44 * Configurator.configure(obj, R.xml.myConfig); 45 * <p/> 46 * WHAT IT DOES: 47 * Given a series of parameters stored in an XML file, Configurator iterates through each parameter, using the name 48 * as a map to the field within a given object. For example: 49 * <p/> 50 * <pre> 51 * {@code 52 * <config car.engine.sparkPlug.condition="poor"/> 53 * } 54 * </pre> 55 * <p/> 56 * Given a Car instance car and assuming the method setCondition(String) exists within the SparkPlug class, 57 * Configurator does the following: 58 * <p/> 59 * <pre> 60 * {@code 61 * car.getEngine().getSparkPlug().setCondition("poor"); 62 * } 63 * </pre> 64 * <p/> 65 * Now let's pretend that setCondition takes an instance of the Condition enum as it's argument. 66 * Configurator then does the following: 67 * <p/> 68 * car.getEngine().getSparkPlug().setCondition(Condition.valueOf("poor"); 69 * <p/> 70 * Now let's look at how ints are handled. Given the following xml: 71 * <p/> 72 * <config car.engine.miles="100000"/> 73 * <p/> 74 * would result in: 75 * car.getEngine.setMiles(Integer.ParseInt("100000"); 76 * <p/> 77 * That's pretty straight forward. But colors are expressed as ints too in Android 78 * but can be defined using hex values or even names of colors. When Configurator 79 * attempts to parse a parameter for a method that it knows takes an int as it's argument, 80 * Configurator will first attempt to parse the parameter as a color. Only after this 81 * attempt fails will Configurator resort to Integer.ParseInt. So: 82 * <p/> 83 * <config car.hood.paint.color="Red"/> 84 * <p/> 85 * would result in: 86 * car.getHood().getPaint().setColor(Color.parseColor("Red"); 87 * <p/> 88 * Next lets talk about float. Floats can appear in XML a few different ways in Android, 89 * especially when it comes to defining dimensions: 90 * <p/> 91 * <config height="10dp" depth="2mm" width="5em"/> 92 * <p/> 93 * Configurator will correctly parse each of these into their corresponding real pixel value expressed as a float. 94 * <p/> 95 * One last thing to keep in mind when using Configurator: 96 * Values for Strings and ints can be assigned to localized values, allowing 97 * a cleaner solution for those developing apps to run on multiple form factors 98 * or in multiple languages: 99 * <p/> 100 * <config thingy.description="@string/thingyDescription" 101 * thingy.titlePaint.textSize=""/> 102 */ 103 @SuppressWarnings("WeakerAccess") 104 public abstract class Configurator { 105 106 private static final String TAG = Configurator.class.getName(); 107 protected static final String CFG_ELEMENT_NAME = "config"; 108 parseResId(Context ctx, String prefix, String value)109 protected static int parseResId(Context ctx, String prefix, String value) { 110 String[] split = value.split("/"); 111 // is this a localized resource? 112 if (split.length > 1 && split[0].equalsIgnoreCase(prefix)) { 113 String pack = split[0].replace("@", ""); 114 String name = split[1]; 115 return ctx.getResources().getIdentifier(name, pack, ctx.getPackageName()); 116 } else { 117 throw new IllegalArgumentException(); 118 } 119 } 120 parseIntAttr(Context ctx, String value)121 protected static int parseIntAttr(Context ctx, String value) { 122 try { 123 return ctx.getResources().getColor(parseResId(ctx, "@color", value)); 124 } catch (IllegalArgumentException e1) { 125 try { 126 return Color.parseColor(value); 127 } catch (IllegalArgumentException e2) { 128 // wasn't a color so try parsing as a plain old int: 129 return Integer.parseInt(value); 130 } 131 } 132 } 133 134 /** 135 * Treats value as a float parameter. First value is tested to see whether 136 * it contains a resource identifier. Failing that, it is tested to see whether 137 * a dimension suffix (dp, em, mm etc.) exists. Failing that, it is evaluated as 138 * a plain old float. 139 * @param ctx 140 * @param value 141 * @return 142 */ parseFloatAttr(Context ctx, String value)143 protected static float parseFloatAttr(Context ctx, String value) { 144 try { 145 return ctx.getResources().getDimension(parseResId(ctx, "@dimen", value)); 146 } catch (IllegalArgumentException e1) { 147 try { 148 return PixelUtils.stringToDimension(value); 149 } catch (Exception e2) { 150 return Float.parseFloat(value); 151 } 152 } 153 } 154 parseStringAttr(Context ctx, String value)155 protected static String parseStringAttr(Context ctx, String value) { 156 try { 157 return ctx.getResources().getString(parseResId(ctx, "@string", value)); 158 } catch (IllegalArgumentException e1) { 159 return value; 160 } 161 } 162 163 getSetter(Class clazz, final String fieldId)164 protected static Method getSetter(Class clazz, final String fieldId) throws NoSuchMethodException { 165 Method[] methods = clazz.getMethods(); 166 167 String methodName = "set" + fieldId; 168 for (Method method : methods) { 169 if (method.getName().equalsIgnoreCase(methodName)) { 170 return method; 171 } 172 } 173 throw new NoSuchMethodException("No such public method (case insensitive): " + 174 methodName + " in " + clazz); 175 } 176 177 @SuppressWarnings("unchecked") getGetter(Class clazz, final String fieldId)178 protected static Method getGetter(Class clazz, final String fieldId) throws NoSuchMethodException { 179 Log.d(TAG, "Attempting to find getter for " + fieldId + " in class " + clazz.getName()); 180 String firstLetter = fieldId.substring(0, 1); 181 String methodName = "get" + firstLetter.toUpperCase() + fieldId.substring(1, fieldId.length()); 182 return clazz.getMethod(methodName); 183 } 184 185 /** 186 * Returns the object containing the field specified by path. 187 * @param obj 188 * @param path Path through member hierarchy to the destination field. 189 * @return null if the object at path cannot be found. 190 * @throws java.lang.reflect.InvocationTargetException 191 * 192 * @throws IllegalAccessException 193 */ getObjectContaining(Object obj, String path)194 protected static Object getObjectContaining(Object obj, String path) throws 195 InvocationTargetException, IllegalAccessException, NoSuchMethodException { 196 if(obj == null) { 197 throw new NullPointerException("Attempt to call getObjectContaining(Object obj, String path) " + 198 "on a null Object instance. Path was: " + path); 199 } 200 Log.d(TAG, "Looking up object containing: " + path); 201 int separatorIndex = path.indexOf("."); 202 203 // not there yet, descend deeper: 204 if (separatorIndex > 0) { 205 String lhs = path.substring(0, separatorIndex); 206 String rhs = path.substring(separatorIndex + 1, path.length()); 207 208 // use getter to retrieve the instance 209 Method m = getGetter(obj.getClass(), lhs); 210 if(m == null) { 211 throw new NullPointerException("No getter found for field: " + lhs + " within " + obj.getClass()); 212 } 213 Log.d(TAG, "Invoking " + m.getName() + " on instance of " + obj.getClass().getName()); 214 Object o = m.invoke(obj); 215 // delve into o 216 return getObjectContaining(o, rhs); 217 //} catch (NoSuchMethodException e) { 218 // TODO: log a warning 219 // return null; 220 //} 221 } else { 222 // found it! 223 return obj; 224 } 225 } 226 227 @SuppressWarnings("unchecked") inflateParams(Context ctx, Class[] params, String[] vals)228 private static Object[] inflateParams(Context ctx, Class[] params, String[] vals) throws NoSuchMethodException, 229 InvocationTargetException, IllegalAccessException { 230 Object[] out = new Object[params.length]; 231 int i = 0; 232 for (Class param : params) { 233 if (Enum.class.isAssignableFrom(param)) { 234 out[i] = param.getMethod("valueOf", String.class).invoke(null, vals[i].toUpperCase()); 235 } else if (param.equals(Float.TYPE)) { 236 out[i] = parseFloatAttr(ctx, vals[i]); 237 } else if (param.equals(Integer.TYPE)) { 238 out[i] = parseIntAttr(ctx, vals[i]); 239 } else if (param.equals(Boolean.TYPE)) { 240 out[i] = Boolean.valueOf(vals[i]); 241 } else if (param.equals(String.class)) { 242 out[i] = parseStringAttr(ctx, vals[i]); 243 } else { 244 throw new IllegalArgumentException( 245 "Error inflating XML: Setter requires param of unsupported type: " + param); 246 } 247 i++; 248 } 249 return out; 250 } 251 252 /** 253 * 254 * @param ctx 255 * @param obj 256 * @param xmlFileId ID of the XML config file within /res/xml 257 */ configure(Context ctx, Object obj, int xmlFileId)258 public static void configure(Context ctx, Object obj, int xmlFileId) { 259 XmlResourceParser xrp = ctx.getResources().getXml(xmlFileId); 260 try { 261 HashMap<String, String> params = new HashMap<String, String>(); 262 while (xrp.getEventType() != XmlResourceParser.END_DOCUMENT) { 263 xrp.next(); 264 String name = xrp.getName(); 265 if (xrp.getEventType() == XmlResourceParser.START_TAG) { 266 if (name.equalsIgnoreCase(CFG_ELEMENT_NAME)) 267 for (int i = 0; i < xrp.getAttributeCount(); i++) { 268 params.put(xrp.getAttributeName(i), xrp.getAttributeValue(i)); 269 } 270 break; 271 } 272 } 273 configure(ctx, obj, params); 274 } catch (XmlPullParserException e) { 275 e.printStackTrace(); 276 } catch (IOException e) { 277 e.printStackTrace(); 278 } finally { 279 xrp.close(); 280 } 281 } 282 configure(Context ctx, Object obj, HashMap<String, String> params)283 public static void configure(Context ctx, Object obj, HashMap<String, String> params) { 284 for (String key : params.keySet()) { 285 try { 286 configure(ctx, obj, key, params.get(key)); 287 } catch (InvocationTargetException e) { 288 e.printStackTrace(); 289 } catch (IllegalAccessException e) { 290 e.printStackTrace(); 291 } catch (NoSuchMethodException e) { 292 Log.w(TAG, "Error inflating XML: Setter for field \"" + key + "\" does not exist. "); 293 e.printStackTrace(); 294 } 295 } 296 } 297 298 /** 299 * Recursively descend into an object using key as the pathway and invoking the corresponding setter 300 * if one exists. 301 * 302 * @param key 303 * @param value 304 */ configure(Context ctx, Object obj, String key, String value)305 protected static void configure(Context ctx, Object obj, String key, String value) 306 throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { 307 Object o = getObjectContaining(obj, key); 308 if (o != null) { 309 int idx = key.lastIndexOf("."); 310 String fieldId = idx > 0 ? key.substring(idx + 1, key.length()) : key; 311 312 Method m = getSetter(o.getClass(), fieldId); 313 Class[] paramTypes = m.getParameterTypes(); 314 // TODO: add support for generic type params 315 if (paramTypes.length >= 1) { 316 317 // split on "|" 318 // TODO: add support for String args containing a | 319 String[] paramStrs = value.split("\\|"); 320 if (paramStrs.length == paramTypes.length) { 321 322 Object[] oa = inflateParams(ctx, paramTypes, paramStrs); 323 Log.d(TAG, "Invoking " + m.getName() + " with arg(s) " + argArrToString(oa)); 324 m.invoke(o, oa); 325 } else { 326 throw new IllegalArgumentException("Error inflating XML: Unexpected number of argments passed to \"" 327 + m.getName() + "\". Expected: " + paramTypes.length + " Got: " + paramStrs.length); 328 } 329 } else { 330 // Obvious this is not a setter 331 throw new IllegalArgumentException("Error inflating XML: no setter method found for param \"" + 332 fieldId + "\"."); 333 } 334 } 335 } 336 argArrToString(Object[] args)337 protected static String argArrToString(Object[] args) { 338 String out = ""; 339 for(Object obj : args) { 340 out += (obj == null ? (out += "[null] ") : 341 ("[" + obj.getClass() + ": " + obj + "] ")); 342 } 343 return out; 344 } 345 } 346 347