001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.util.api.auth; 031 032import java.io.BufferedReader; 033import java.io.IOException; 034import java.io.InputStreamReader; 035import java.lang.reflect.Field; 036import java.util.ArrayList; 037import java.util.Collection; 038import java.util.HashMap; 039import java.util.List; 040import java.util.Map; 041import java.util.Map.Entry; 042import java.util.prefs.BackingStoreException; 043import java.util.prefs.Preferences; 044 045import org.apache.commons.lang.ArrayUtils; 046import org.apache.commons.lang.WordUtils; 047 048/** 049 * Default implementation of a {@link TokenFactory} that loads the token 050 * parameters from the default Java user preference store or interactively 051 * queries the user for the required token parameters if the token has not been 052 * used before. 053 * <p> 054 * Interactive querying is performed via the command-line (using 055 * {@link System#err} for prompts and {@link System#in} for reading user input. 056 * As such, this class will only be really useful for interactive querying in 057 * console applications. It is possible however to just use this class for 058 * manually storing and retrieving tokens with the appropriate methods. 059 * <p> 060 * For this class to work in interactive mode, the token class must have a 061 * public no-args constructor. 062 * 063 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 064 * 065 */ 066public class DefaultTokenFactory implements TokenFactory { 067 private static final DefaultTokenFactory instance = new DefaultTokenFactory(); 068 private static final String PREFS_BASE_NODE = "/org/openimaj/util/api/auth"; 069 070 private DefaultTokenFactory() { 071 } 072 073 /** 074 * Get the default singleton instance 075 * 076 * @return the default instance 077 */ 078 public static DefaultTokenFactory getInstance() { 079 return instance; 080 } 081 082 /** 083 * Delete the default token parameters for the given class from the store. 084 * 085 * @param tokenClass 086 * the token class 087 * @throws BackingStoreException 088 * if a problem occurred communicating with the backing 089 * preference store 090 */ 091 public <T> void deleteToken(Class<T> tokenClass) throws BackingStoreException { 092 deleteToken(tokenClass, null); 093 } 094 095 /** 096 * Delete the named token parameters for the given class from the store. 097 * 098 * @param tokenClass 099 * the token class 100 * @param name 101 * the name of the token, or <tt>null</tt> for the default token 102 * @throws BackingStoreException 103 * if a problem occurred communicating with the backing 104 * preference store 105 */ 106 public <T> void deleteToken(Class<T> tokenClass, String name) throws BackingStoreException { 107 final String tokName = name == null ? tokenClass.getName() : tokenClass.getName() + "-" + name; 108 final Preferences base = Preferences.userRoot().node(PREFS_BASE_NODE); 109 110 base.node(tokName).removeNode(); 111 112 base.sync(); 113 } 114 115 @Override 116 public <T> T getToken(Class<T> tokenClass) { 117 return getToken(tokenClass, null); 118 } 119 120 @Override 121 public <T> T getToken(Class<T> tokenClass, String name) { 122 final Token tokenDef = tokenClass.getAnnotation(Token.class); 123 124 if (tokenDef == null) 125 throw new IllegalArgumentException("The provided class is not annotated with @Token"); 126 127 try { 128 T token = loadToken(tokenClass, name); 129 130 if (token == null) { 131 token = createToken(tokenDef, tokenClass, name); 132 } 133 134 return token; 135 } catch (final Exception e) { 136 throw new RuntimeException(e); 137 } 138 } 139 140 private String getMessage(Token def, Map<Field, Parameter> params) { 141 String msg = String.format("You do not appear to have any credentials stored for the %s. ", def.name()) + 142 String.format("To use the %s you need to have a %s.\n", def.name(), formatParams(params.values())) + 143 String.format("You can get these from %s.\n\n", def.url()); 144 145 if (def.extraInfo() != null && def.extraInfo().length() > 0) 146 msg += String.format(def.extraInfo() + "\n\n"); 147 148 msg += String.format("To continue please enter the credentials as indicated. "); 149 msg += String.format("These will be stored automatically for future use."); 150 151 return msg; 152 } 153 154 private <T> T createToken(Token def, Class<T> clz, String name) throws InstantiationException, 155 IllegalAccessException, 156 IOException, IllegalArgumentException, BackingStoreException 157 { 158 final Map<Field, Parameter> params = getParameters(clz); 159 160 final T instance = clz.newInstance(); 161 if (params.size() == 0) 162 return instance; 163 164 System.err.format(WordUtils.wrap(getMessage(def, params) + "\n\n", 80)); 165 166 final BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 167 168 final Map<Parameter, String> inputs = new HashMap<Parameter, String>(); 169 for (final Entry<Field, Parameter> entry : params.entrySet()) { 170 query(instance, entry.getKey(), entry.getValue(), br, inputs); 171 } 172 173 System.err.println("\n\n"); 174 System.err.println("The following parameters have been set:\n"); 175 for (final Entry<Field, Parameter> entry : params.entrySet()) { 176 System.err.println(entry.getValue().name() + ": " + inputs.get(entry.getValue()) + "\n"); 177 } 178 179 while (true) { 180 System.err.println("\nPlease confirm the parameters are correct (Y/N): "); 181 if (br.readLine().trim().equalsIgnoreCase("y")) { 182 break; 183 } else if (br.readLine().trim().equalsIgnoreCase("n")) { 184 return createToken(def, clz, name); 185 } 186 } 187 188 saveToken(instance, name); 189 190 return instance; 191 } 192 193 /** 194 * Save the parameters of the given token to the backing preference store. 195 * 196 * @param token 197 * the token to save 198 * @param name 199 * the name of the token, or <tt>null</tt> for the default 200 * @throws IllegalArgumentException 201 * if the token class isn't annotated with {@link Token}. 202 * @throws IllegalAccessException 203 * if an error occurred reading a parameter of the token 204 * @throws BackingStoreException 205 * if a problem occurred communicating with the backing 206 * preference store 207 */ 208 public <T> void saveToken(T token, String name) throws IllegalArgumentException, IllegalAccessException, 209 BackingStoreException 210 { 211 212 final Class<?> tokenClass = token.getClass(); 213 final String tokName = name == null ? tokenClass.getName() : tokenClass.getName() + "-" + name; 214 215 final Token tokenDef = tokenClass.getAnnotation(Token.class); 216 217 if (tokenDef == null) 218 throw new IllegalArgumentException("The provided class is not annotated with @Token"); 219 220 final Preferences prefs = Preferences.userRoot().node(PREFS_BASE_NODE).node(tokName); 221 final Map<Field, Parameter> params = getParameters(tokenClass); 222 223 for (final Entry<Field, Parameter> entry : params.entrySet()) { 224 final Field f = entry.getKey(); 225 226 if (f.getType() == Integer.class) { 227 prefs.putInt(f.getName(), (Integer) f.get(token)); 228 } else if (f.getType() == Integer.TYPE) { 229 prefs.putInt(f.getName(), f.getInt(token)); 230 } else if (f.getType() == Long.class) { 231 prefs.putLong(f.getName(), (Long) f.get(token)); 232 } else if (f.getType() == Long.TYPE) { 233 prefs.putLong(f.getName(), f.getLong(token)); 234 } else if (f.getType() == Double.class) { 235 prefs.putDouble(f.getName(), (Double) f.get(token)); 236 } else if (f.getType() == Double.TYPE) { 237 prefs.putDouble(f.getName(), f.getDouble(token)); 238 } else if (f.getType() == Float.class) { 239 prefs.putFloat(f.getName(), (Float) f.get(token)); 240 } else if (f.getType() == Float.TYPE) { 241 prefs.putFloat(f.getName(), f.getFloat(token)); 242 } else if (f.getType() == String.class) { 243 prefs.put(f.getName(), (String) f.get(token)); 244 } else if (f.getType() == byte[].class) { 245 prefs.putByteArray(f.getName(), (byte[]) f.get(token)); 246 } 247 } 248 249 prefs.sync(); 250 } 251 252 private void query(Object instance, Field f, Parameter p, BufferedReader br, Map<Parameter, String> rawInputs) 253 throws IOException, 254 IllegalArgumentException, IllegalAccessException 255 { 256 257 while (true) { 258 System.err.format("Please enter your %s:\n", p.name()); 259 final String input = br.readLine().trim(); 260 261 rawInputs.put(p, input); 262 263 if (setValue(instance, f, input)) 264 return; 265 266 System.err.format("Sorry, %s doesn't appear to be the correct format for the %s (hint: expecting a %s).\n", 267 input, p.name(), getType(f)); 268 } 269 } 270 271 private boolean setValue(Object instance, Field f, String input) throws IllegalArgumentException, 272 IllegalAccessException 273 { 274 try { 275 if (f.getType() == Integer.class) 276 f.set(instance, Integer.parseInt(input)); 277 else if (f.getType() == Integer.TYPE) 278 f.setInt(instance, Integer.parseInt(input)); 279 if (f.getType() == Long.class) 280 f.set(instance, Long.parseLong(input)); 281 else if (f.getType() == Long.TYPE) 282 f.setLong(instance, Long.parseLong(input)); 283 if (f.getType() == Double.class) 284 f.set(instance, Double.parseDouble(input)); 285 else if (f.getType() == Double.TYPE) 286 f.setDouble(instance, Double.parseDouble(input)); 287 if (f.getType() == Float.class) 288 f.set(instance, Float.parseFloat(input)); 289 else if (f.getType() == Float.TYPE) 290 f.setFloat(instance, Float.parseFloat(input)); 291 if (f.getType() == String.class) 292 f.set(instance, input); 293 if (f.getType() == byte[].class) 294 f.set(instance, input.getBytes()); 295 } catch (final NumberFormatException nfe) { 296 return false; 297 } 298 299 return true; 300 } 301 302 private String getType(Field f) { 303 if (f.getType() == Integer.class || f.getType() == Integer.TYPE) 304 return "integer"; 305 if (f.getType() == Long.class || f.getType() == Long.TYPE) 306 return "long"; 307 if (f.getType() == Double.class || f.getType() == Double.TYPE) 308 return "double"; 309 if (f.getType() == Float.class || f.getType() == Float.TYPE) 310 return "float"; 311 if (f.getType() == String.class) 312 return "string"; 313 if (f.getType() == byte[].class) 314 return "byte array"; 315 316 throw new UnsupportedOperationException("Unsupported field type " + f.getType() + " for field " 317 + f.getName()); 318 } 319 320 private String formatParams(Collection<Parameter> values) { 321 final List<Parameter> paramsList = new ArrayList<Parameter>(values); 322 323 if (values.size() == 1) 324 return paramsList.get(0).name(); 325 326 final StringBuilder sb = new StringBuilder(); 327 for (int i = 0; i < paramsList.size() - 2; i++) 328 sb.append(paramsList.get(i).name() + ", "); 329 sb.append(paramsList.get(paramsList.size() - 2).name()); 330 sb.append(" and "); 331 sb.append(paramsList.get(paramsList.size() - 1).name()); 332 333 return sb.toString(); 334 } 335 336 private Map<Field, Parameter> getParameters(Class<?> clz) { 337 final Map<Field, Parameter> fields = new HashMap<Field, Parameter>(); 338 339 while (clz != null) { 340 for (final Field f : clz.getDeclaredFields()) { 341 final Parameter p = f.getAnnotation(Parameter.class); 342 if (p != null) { 343 f.setAccessible(true); 344 fields.put(f, p); 345 } 346 } 347 348 clz = clz.getSuperclass(); 349 } 350 351 return fields; 352 } 353 354 /** 355 * Load a token with an optional name tag from the backing store. 356 * 357 * @param clz 358 * the class of the token 359 * @param name 360 * the name of the token, or <tt>null</tt> for the default token 361 * @return a token loaded with the previously saved parameters, or 362 * <tt>null</tt> if the token could not be read 363 * @throws BackingStoreException 364 * if a problem occurred communicating with the backing 365 * preference store 366 * @throws InstantiationException 367 * if the token could not be constructed 368 * @throws IllegalAccessException 369 * if an error occurred setting a parameter 370 */ 371 public <T> T loadToken(Class<T> clz, String name) throws BackingStoreException, InstantiationException, 372 IllegalAccessException 373 { 374 final String tokName = name == null ? clz.getName() : clz.getName() + "-" + name; 375 Preferences prefs = Preferences.userRoot().node(PREFS_BASE_NODE); 376 377 if (!prefs.nodeExists(tokName)) 378 return null; 379 380 prefs = prefs.node(tokName); 381 final String[] keys = prefs.keys(); 382 383 final T instance = clz.newInstance(); 384 385 final Map<Field, Parameter> params = getParameters(clz); 386 for (final Entry<Field, Parameter> p : params.entrySet()) { 387 final Field field = p.getKey(); 388 final Parameter parameter = p.getValue(); 389 390 if (!ArrayUtils.contains(keys, field.getName())) 391 return null; // missing value in store, so force new one to be 392 // created 393 394 loadValue(instance, field, parameter, prefs); 395 } 396 397 return instance; 398 } 399 400 private <T> void loadValue(T instance, Field field, Parameter parameter, Preferences node) 401 throws IllegalArgumentException, IllegalAccessException 402 { 403 final String fieldName = field.getName(); 404 405 if (field.getType() == Integer.TYPE) { 406 field.setInt(instance, node.getInt(fieldName, 0)); 407 } else if (field.getType() == Integer.class) { 408 field.set(instance, node.getInt(fieldName, 0)); 409 } else if (field.getType() == Long.TYPE) { 410 field.setLong(instance, node.getLong(fieldName, 0)); 411 } else if (field.getType() == Long.class) { 412 field.set(instance, node.getLong(fieldName, 0)); 413 } else if (field.getType() == Double.TYPE) { 414 field.setDouble(instance, node.getDouble(fieldName, 0)); 415 } else if (field.getType() == Double.class) { 416 field.set(instance, node.getDouble(fieldName, 0)); 417 } else if (field.getType() == Float.TYPE) { 418 field.setFloat(instance, node.getFloat(fieldName, 0)); 419 } else if (field.getType() == Float.class) { 420 field.set(instance, node.getFloat(fieldName, 0)); 421 } else if (field.getType() == Boolean.TYPE) { 422 field.setBoolean(instance, node.getBoolean(fieldName, false)); 423 } else if (field.getType() == Boolean.class) { 424 field.set(instance, node.getBoolean(fieldName, false)); 425 } else if (field.getType() == String.class) { 426 field.set(instance, node.get(fieldName, null)); 427 } else if (field.getType() == byte[].class) { 428 field.set(instance, node.getByteArray(fieldName, null)); 429 } else { 430 throw new UnsupportedOperationException("Unsupported field type " + field.getType() + " for field " 431 + fieldName); 432 } 433 } 434 435 /** 436 * Convenience method equivalent to 437 * <tt>getInstance().getToken(tokenClass)</tt>. 438 * 439 * @see #getToken(Class) 440 * @param tokenClass 441 * the class of the token to build 442 * @return the token 443 */ 444 public static <T> T get(Class<T> tokenClass) { 445 return getInstance().getToken(tokenClass); 446 } 447 448 /** 449 * Convenience method equivalent to 450 * <tt>getInstance().getToken(tokenClass, name)</tt>. 451 * 452 * @see #getToken(Class, String) 453 * @param tokenClass 454 * the class of the token to build 455 * @param name 456 * the name of the token 457 * @return the token 458 */ 459 public static <T> T get(Class<T> tokenClass, String name) { 460 return getInstance().getToken(tokenClass); 461 } 462 463 /** 464 * Convenience method equivalent to 465 * {@code getInstance().deleteToken(tokenClass) } 466 * 467 * @see #deleteToken(Class) 468 * @param tokenClass the class of the token to delete 469 * @throws BackingStoreException 470 * if a problem occurred communicating with the backing 471 * preference store 472 */ 473 public static <T> void delete(Class<T> tokenClass) throws BackingStoreException { 474 getInstance().deleteToken(tokenClass); 475 } 476 477 /** 478 * Convenience method equivalent to 479 * {@code getInstance().deleteToken(tokenClass, name) } 480 * 481 * @see #deleteToken(Class, String) 482 * @param tokenClass the class of the token to delete 483 * @param name the name of the token, or {@code null} for the default token 484 * @throws BackingStoreException 485 * if a problem occurred communicating with the backing 486 * preference store 487 */ 488 public static <T> void delete(Class<T> tokenClass, String name) throws BackingStoreException { 489 getInstance().deleteToken(tokenClass, name); 490 } 491}