001/* 002 * Copyright 2006 - 2013 003 * Stefan Balev <stefan.balev@graphstream-project.org> 004 * Julien Baudry <julien.baudry@graphstream-project.org> 005 * Antoine Dutot <antoine.dutot@graphstream-project.org> 006 * Yoann Pigné <yoann.pigne@graphstream-project.org> 007 * Guilhelm Savin <guilhelm.savin@graphstream-project.org> 008 * 009 * This file is part of GraphStream <http://graphstream-project.org>. 010 * 011 * GraphStream is a library whose purpose is to handle static or dynamic 012 * graph, create them from scratch, file or any source and display them. 013 * 014 * This program is free software distributed under the terms of two licenses, the 015 * CeCILL-C license that fits European law, and the GNU Lesser General Public 016 * License. You can use, modify and/ or redistribute the software under the terms 017 * of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following 018 * URL <http://www.cecill.info> or under the terms of the GNU LGPL as published by 019 * the Free Software Foundation, either version 3 of the License, or (at your 020 * option) any later version. 021 * 022 * This program is distributed in the hope that it will be useful, but WITHOUT ANY 023 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 024 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 025 * 026 * You should have received a copy of the GNU Lesser General Public License 027 * along with this program. If not, see <http://www.gnu.org/licenses/>. 028 * 029 * The fact that you are presently reading this means that you have had 030 * knowledge of the CeCILL-C and LGPL licenses and that you accept their terms. 031 */ 032package org.graphstream.algorithm; 033 034import java.io.FileInputStream; 035import java.io.IOException; 036import java.io.InputStream; 037import java.lang.reflect.Field; 038import java.lang.reflect.InvocationTargetException; 039import java.lang.reflect.Method; 040import java.util.HashMap; 041import java.util.InvalidPropertiesFormatException; 042import java.util.LinkedList; 043import java.util.Properties; 044 045import org.graphstream.algorithm.DefineParameter; 046import org.graphstream.algorithm.InvalidParameterException; 047import org.graphstream.algorithm.MissingParameterException; 048 049/** 050 * Defines a parameter as an association between a String and an Object. 051 */ 052public class Parameter { 053 /** 054 * Shortcut for "new Parameter(key,value)". 055 * 056 * @param key 057 * key of the parameter 058 * @param value 059 * value of the parameter 060 * @return new Parameter(key,value) 061 */ 062 public static final Parameter parameter(String key, Object value) { 063 return new Parameter(key, value); 064 } 065 066 /** 067 * Process parameters. Throws an exception if something is wrong. 068 * 069 * @param env 070 * object to set attributes 071 * @param params 072 * parameters 073 * @throws InvalidParameterException 074 */ 075 public static void processParameters(Object env, Parameter... params) 076 throws InvalidParameterException, MissingParameterException { 077 // 078 // We are lazy, if no parameters there is no need to work ! 079 // 080 if (params == null || params.length == 0) 081 return; 082 083 // 084 // Create a new ParametersProcessor to process the parameters and 085 // run it. 086 // 087 ParametersProcessor pp = new ParametersProcessor(env, params); 088 pp.process(); 089 } 090 091 public static void processParameters(Object env, Properties prop) 092 throws InvalidParameterException, MissingParameterException { 093 // 094 // Create a new ParametersProcessor to process the parameters and 095 // run it. 096 // 097 ParametersProcessor pp = new ParametersProcessor(env, prop); 098 pp.process(); 099 } 100 101 public static void processParameters(Object env, String propertiesPath) 102 throws InvalidParameterException, MissingParameterException, InvalidPropertiesFormatException, IOException { 103 boolean xml = propertiesPath.endsWith(".xml"); 104 FileInputStream in = new FileInputStream(propertiesPath); 105 106 processParameters(env, in, xml); 107 108 in.close(); 109 } 110 public static void processParameters(Object env, InputStream in, boolean xml) 111 throws InvalidParameterException, MissingParameterException, InvalidPropertiesFormatException, IOException { 112 Properties prop = new Properties(); 113 114 if (xml) 115 prop.loadFromXML(in); 116 else 117 prop.load(in); 118 119 processParameters(env, prop); 120 } 121 122 public static Properties exportParameters(Object env) { 123 ParametersProcessor pp = new ParametersProcessor(env); 124 Properties prop = new Properties(); 125 126 for (Field f : pp.fields.keySet()) { 127 DefineParameter dp = f.getAnnotation(DefineParameter.class); 128 129 try { 130 f.setAccessible(true); 131 } catch (Exception e) { 132 // Can't change permission... 133 // Trying anyway to continue ! 134 } 135 136 try { 137 prop.setProperty(dp.name(), f.get(env).toString()); 138 } catch (IllegalArgumentException e) { 139 e.printStackTrace(); 140 } catch (IllegalAccessException e) { 141 e.printStackTrace(); 142 } 143 } 144 145 return prop; 146 } 147 148 /** 149 * Defines the object which will process parameters. 150 * 151 */ 152 public static class ParametersProcessor { 153 HashMap<String, Field> definedParameters; 154 HashMap<Field, Object> fields; 155 HashMap<String, Object> params; 156 157 public ParametersProcessor(Object obj) { 158 init(obj); 159 } 160 161 public ParametersProcessor(Object obj, Properties prop) { 162 init(obj); 163 buildParametersMap(prop); 164 } 165 166 public ParametersProcessor(Object obj, Parameter... parameters) { 167 init(obj); 168 169 if (parameters != null) 170 buildParametersMap(parameters); 171 } 172 173 private void init(Object obj) { 174 fields = new HashMap<Field, Object>(); 175 params = new HashMap<String, Object>(); 176 definedParameters = new HashMap<String, Field>(); 177 178 if (obj.getClass().isArray()) { 179 Object[] objects = (Object[]) obj; 180 181 for (Object o : objects) 182 buildFields(o); 183 } else { 184 buildFields(obj); 185 } 186 } 187 188 /** 189 * Start the processing part. First, this checks is all non-optional 190 * parameters will receive a value, else a MissingParameterException is 191 * thrown. Then, values are assigned to the attribute ( 192 * {@link ParametersProcessor#setValue(DefineParameter, Field, Object)} 193 * ). Finally, the method checks is no parameter remaining. In this 194 * case, an InvalidParameterException is thrown. 195 * 196 * @throws InvalidParameterException 197 * @throws MissingParameterException 198 */ 199 public void process() throws InvalidParameterException, 200 MissingParameterException { 201 checkNonOptionalParameters(); 202 203 LinkedList<String> remainingParameters = new LinkedList<String>( 204 params.keySet()); 205 206 for (Field f : fields.keySet()) { 207 final DefineParameter dp = f 208 .getAnnotation(DefineParameter.class); 209 210 if (params.containsKey(dp.name())) { 211 // 212 // We try to make the field accessible, 213 // if it is a protected or private field. 214 // 215 try { 216 f.setAccessible(true); 217 } catch (Exception e) { 218 // Can't change permission... 219 // Trying anyway to continue ! 220 } 221 222 final Object value = params.get(dp.name()); 223 224 setValue(dp, f, value); 225 226 remainingParameters.remove(dp.name()); 227 } 228 } 229 230 // 231 // If values remain in paramsMap, user try to define parameters 232 // which do not exist. 233 // 234 if (remainingParameters.size() > 0) { 235 String uneatenParams = ""; 236 237 for (String s : remainingParameters) 238 uneatenParams += String.format("%s\"%s\"", (uneatenParams 239 .length() > 0 ? ", " : ""), s); 240 241 throw new InvalidParameterException( 242 "some parameters does not exist : %s", uneatenParams); 243 } 244 } 245 246 /** 247 * Find fields owning a DefineParameter annotation. 248 * 249 * @param obj 250 * the object on which searching fields. 251 */ 252 protected void buildFields(Object obj) { 253 Class<?> cls = obj.getClass(); 254 255 while (cls != Object.class) { 256 Field[] clsFields = cls.getDeclaredFields(); 257 258 if (clsFields != null) { 259 for (Field f : clsFields) { 260 DefineParameter dp = f 261 .getAnnotation(DefineParameter.class); 262 263 if (dp != null) { 264 fields.put(f, obj); 265 definedParameters.put(dp.name(), f); 266 } 267 } 268 } 269 270 cls = cls.getSuperclass(); 271 } 272 } 273 274 /** 275 * Create a map "key -> value" for parameters. 276 * 277 * @param parameters 278 * parameters which will be used in the process part. 279 */ 280 protected void buildParametersMap(Parameter... parameters) { 281 for (Parameter p : parameters) 282 params.put(p.getKey(), p.getValue()); 283 } 284 285 protected void buildParametersMap(Properties prop) { 286 for (String key : prop.stringPropertyNames()) { 287 Field f = definedParameters.get(key); 288 String v = prop.getProperty(key); 289 290 Class<?> type = f.getType(); 291 292 if (type.equals(String.class)) 293 params.put(key, v); 294 else if (type.equals(Double.class) || type.equals(Double.TYPE)) 295 params.put(key, Double.valueOf(v)); 296 else if (type.equals(Float.class) || type.equals(Float.TYPE)) 297 params.put(key, Float.valueOf(v)); 298 else if (type.equals(Integer.class) || type.equals(Integer.TYPE)) 299 params.put(key, Integer.valueOf(v)); 300 else if (type.equals(Long.class) || type.equals(Long.TYPE)) 301 params.put(key, Long.valueOf(v)); 302 else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE)) 303 params.put(key, Boolean.valueOf(v)); 304 else if (type.isEnum()) { 305 @SuppressWarnings("unchecked") 306 Class<? extends Enum<?>> e = (Class<? extends Enum<?>>) type; 307 308 for (Enum<?> en : e.getEnumConstants()) { 309 if (en.name().equals(v)) { 310 params.put(key, en); 311 break; 312 } 313 } 314 } else 315 throw new UnsupportedOperationException(type.getName()); 316 } 317 } 318 319 /** 320 * Set the value of a field according to a parameter. First, the value 321 * is check ( {@link #checkValue(DefineParameter, Field, Object)} ). 322 * Then, the beforeSet trigger is called ( 323 * {@link #callBeforeSetTrigger(DefineParameter, Field, Object)} ). 324 * Then, if 'setters' value is empty, value is simply assigned to the 325 * field, else 'setter' is called ( 326 * {@link #callSetter(DefineParameter, Field, Object)} ). Finally, the 327 * afterSet trigger is called ( 328 * {@link #callAfterSetTrigger(DefineParameter, Field, Object)}). 329 * 330 * @param dp 331 * the DefineParameter annotation being set. 332 * @param f 333 * field associated to the annotation. 334 * @param value 335 * value which will be assigned to the field. 336 * @throws InvalidParameterException 337 */ 338 protected void setValue(DefineParameter dp, Field f, Object value) 339 throws InvalidParameterException { 340 checkValue(dp, f, value); 341 342 Object env = fields.get(f); 343 344 callBeforeSetTrigger(dp, f, value); 345 346 if (dp.setter().length() == 0) { 347 try { 348 f.set(env, value); 349 } catch (IllegalArgumentException e) { 350 throw new InvalidParameterException( 351 "invalid value type for %s, %s expected", 352 dp.name(), f.getType().getName()); 353 } catch (IllegalAccessException e) { 354 throw new InvalidParameterException( 355 "parameter value can not be set. maybe a permission problem"); 356 } 357 } else { 358 callSetter(dp, f, value); 359 } 360 361 callAfterSetTrigger(dp, f, value); 362 } 363 364 /** 365 * Check is the value is valid according to the DefineParameter 366 * annotation. 367 * 368 * @param dp 369 * the DefineParameter annotation associated to the field. 370 * @param f 371 * the field. 372 * @param value 373 * the value. 374 * @throws InvalidParameterException 375 */ 376 protected void checkValue(DefineParameter dp, Field f, Object value) 377 throws InvalidParameterException { 378 final boolean isNumber = value instanceof Number; 379 380 // 381 // If type is defined, value should be assignable to this 382 // type. 383 // 384 if (dp.type() != Object.class 385 && !dp.type().isAssignableFrom(value.getClass())) 386 throw new InvalidParameterException( 387 "invalid parameter type, should be %s", dp.type() 388 .getName()); 389 390 // 391 // If min or max are defined, value should be a number 392 // between min and max. 393 // 394 if (!Double.isNaN(dp.min()) || !Double.isNaN(dp.max())) { 395 if (!isNumber) 396 throw new InvalidParameterException( 397 "min or max defined but value is not a number for %s", 398 dp.name()); 399 400 Number n = (Number) value; 401 402 if (dp.min() != Double.NaN && n.doubleValue() < dp.min()) 403 throw new InvalidParameterException(String.format( 404 "bad value for \"%s\", %f < min", dp.name(), n 405 .doubleValue())); 406 407 if (dp.max() != Double.NaN && n.doubleValue() > dp.max()) 408 throw new InvalidParameterException(String.format( 409 "bad value for \"%s\", %f > max", dp.name(), n 410 .doubleValue())); 411 } 412 413 // 414 // If strings is defined, value should be a String and must 415 // be one of the defined values. 416 // 417 if (dp.strings().length > 0) { 418 if (value.getClass() != String.class) 419 throw new InvalidParameterException( 420 "value should be a String"); 421 422 String s = (String) value; 423 boolean found = false; 424 425 for (String alt : dp.strings()) 426 if (alt.equals(s)) { 427 found = true; 428 break; 429 } 430 431 if (!found) 432 throw new InvalidParameterException( 433 "\"%s\" is not in the allowed values for %s", 434 value, dp.name()); 435 } 436 } 437 438 /** 439 * Check if all non-optional parameters will receive a value. 440 * 441 * @throws MissingParameterException 442 * a non-optional parameter does not receive its value. 443 */ 444 protected void checkNonOptionalParameters() 445 throws MissingParameterException { 446 for (Field f : fields.keySet()) { 447 DefineParameter dp = f.getAnnotation(DefineParameter.class); 448 449 if (!dp.optional() && !params.containsKey(dp.name())) 450 throw new MissingParameterException( 451 "parameter \"%s\" is missing", dp.name()); 452 } 453 } 454 455 /** 456 * Call setter of a parameter. This is called when 457 * {@link DefineParameter#setter()} is not empty. If arguments count of 458 * the setter is 1, then the value is passed as argument. If count is 2, 459 * then parameter name and value are passed as arguments. Else, a 460 * InvalidParameterException is thrown. 461 * 462 * @param dp 463 * the DefineParameter annotation associated to the field. 464 * @param f 465 * the field. 466 * @param value 467 * the value. 468 * @throws InvalidParameterException 469 */ 470 protected void callSetter(DefineParameter dp, Field f, Object value) 471 throws InvalidParameterException { 472 Object env = fields.get(f); 473 474 Method setter = null; 475 476 { 477 Class<?> cls = env.getClass(); 478 479 while (setter == null && cls != Object.class) { 480 Method[] methods = cls.getDeclaredMethods(); 481 482 if (methods != null) { 483 for (Method m : methods) 484 if (m.getName().equals(dp.setter())) { 485 setter = m; 486 break; 487 } 488 } 489 490 cls = cls.getSuperclass(); 491 } 492 } 493 494 if (setter == null) 495 throw new InvalidParameterException( 496 "'setter' '%s()' can not be found for %s", dp.setter(), 497 dp.name()); 498 499 Object[] args = null; 500 501 switch (setter.getParameterTypes().length) { 502 case 1: 503 // If trigger has one argument, we pass the value of 504 // the parameter. 505 args = new Object[] { value }; 506 break; 507 case 2: 508 // If trigger has two arguments, we pass the key and 509 // the value of the parameter. 510 args = new Object[] { dp.name(), value }; 511 break; 512 default: 513 throw new InvalidParameterException( 514 "bad argument count in 'setter' '%s()' for %s", dp 515 .setter(), dp.name()); 516 } 517 518 try { 519 setter.invoke(env, args); 520 } catch (IllegalArgumentException e) { 521 throw new InvalidParameterException( 522 "bad arguments in 'setter' '%s()'for %s", dp.setter(), 523 dp.name()); 524 } catch (IllegalAccessException e) { 525 throw new InvalidParameterException( 526 "illegal access to 'setter' '%s()' for %s", 527 dp.setter(), dp.name()); 528 } catch (InvocationTargetException e) { 529 throw new InvalidParameterException( 530 "invocation error of 'setter' '%s()' for %s", dp 531 .setter(), dp.name()); 532 } 533 } 534 535 /** 536 * Call the beforeSet trigger. The name of the method to call can be 537 * defined in @link {@link DefineParameter#beforeSet()}. If arguments 538 * count of the method is 0, no argument is given. If 1, then the value 539 * is given. If 2, then the parameter name and its value are given. 540 * Else, throw an InvalidParameterException. 541 * 542 * @param dp 543 * the DefineParameter annotation associated to the field. 544 * @param f 545 * the field. 546 * @param value 547 * the value. 548 * @throws InvalidParameterException 549 */ 550 protected void callBeforeSetTrigger(DefineParameter dp, Field f, 551 Object value) throws InvalidParameterException { 552 if (dp.beforeSet().length() > 0) { 553 Object env = fields.get(f); 554 555 Method beforeSet = null; 556 557 { 558 Class<?> cls = env.getClass(); 559 560 while (beforeSet == null && cls != Object.class) { 561 Method[] methods = cls.getDeclaredMethods(); 562 563 if (methods != null) { 564 for (Method m : methods) 565 if (m.getName().equals(dp.beforeSet())) { 566 beforeSet = m; 567 break; 568 } 569 } 570 571 cls = cls.getSuperclass(); 572 } 573 } 574 575 if (beforeSet == null) 576 throw new InvalidParameterException( 577 "'beforeSet' trigger '%s()' can not be found for %s", 578 dp.beforeSet(), dp.name()); 579 580 Object[] args = null; 581 582 switch (beforeSet.getParameterTypes().length) { 583 case 0: 584 // Nothing 585 break; 586 case 1: 587 // If trigger has one argument, we pass the value of 588 // the parameter. 589 args = new Object[] { value }; 590 break; 591 case 2: 592 // If trigger has two arguments, we pass the key and 593 // the value of the parameter. 594 args = new Object[] { dp.name(), value }; 595 break; 596 default: 597 throw new InvalidParameterException( 598 "two much arguments in 'beforeSet' trigger '%s()' for %s", 599 dp.beforeSet(), dp.name()); 600 } 601 602 try { 603 beforeSet.invoke(env, args); 604 } catch (IllegalArgumentException e) { 605 throw new InvalidParameterException( 606 "bad arguments in 'beforeSet' trigger '%s()'for %s", 607 dp.beforeSet(), dp.name()); 608 } catch (IllegalAccessException e) { 609 throw new InvalidParameterException( 610 "illegal access to 'beforeSet' trigger '%s()' for %s", 611 dp.beforeSet(), dp.name()); 612 } catch (InvocationTargetException e) { 613 throw new InvalidParameterException( 614 "invocation error of 'beforeSet' trigger '%s()' for %s", 615 dp.beforeSet(), dp.name()); 616 } 617 } 618 } 619 620 /** 621 * Call the afterSet trigger. The name of the method to call can be 622 * defined in @link {@link DefineParameter#afterSet()}. If arguments 623 * count of the method is 0, no argument is given. If 1, then the value 624 * is given. If 2, then the parameter name and its value are given. 625 * Else, throw an InvalidParameterException. 626 * 627 * @param dp 628 * the DefineParameter annotation associated to the field. 629 * @param f 630 * the field. 631 * @param value 632 * the value. 633 * @throws InvalidParameterException 634 */ 635 protected void callAfterSetTrigger(DefineParameter dp, Field f, 636 Object value) throws InvalidParameterException { 637 Object env = fields.get(f); 638 639 if (dp.afterSet().length() > 0) { 640 Method afterSet = null; 641 642 { 643 Class<?> cls = env.getClass(); 644 645 while (afterSet == null && cls != Object.class) { 646 Method[] methods = cls.getDeclaredMethods(); 647 648 if (methods != null) { 649 for (Method m : methods) 650 if (m.getName().equals(dp.afterSet())) { 651 afterSet = m; 652 break; 653 } 654 } 655 656 cls = cls.getSuperclass(); 657 } 658 } 659 660 if (afterSet == null) 661 throw new InvalidParameterException( 662 "'afterSet' trigger '%s()' can not be found for %s", 663 dp.afterSet(), dp.name()); 664 665 Object[] args = null; 666 667 switch (afterSet.getParameterTypes().length) { 668 case 0: 669 // Nothing 670 break; 671 case 1: 672 // If trigger has one argument, we pass the value of 673 // the parameter. 674 args = new Object[] { value }; 675 break; 676 case 2: 677 // If trigger has two arguments, we pass the key and 678 // the value of the parameter. 679 args = new Object[] { dp.name(), value }; 680 break; 681 default: 682 throw new InvalidParameterException( 683 "two much arguments in 'afterSet' trigger '%s()' for %s", 684 dp.afterSet(), dp.name()); 685 } 686 687 try { 688 afterSet.invoke(env, args); 689 } catch (IllegalArgumentException e) { 690 throw new InvalidParameterException( 691 "bad arguments in 'afterSet' trigger '%s()'for %s", 692 dp.afterSet(), dp.name()); 693 } catch (IllegalAccessException e) { 694 throw new InvalidParameterException( 695 "illegal access to 'afterSet' trigger '%s()' for %s", 696 dp.afterSet(), dp.name()); 697 } catch (InvocationTargetException e) { 698 throw new InvalidParameterException( 699 "invocation error of 'afterSet' trigger '%s()' for %s", 700 dp.afterSet(), dp.name()); 701 } 702 } 703 } 704 } 705 706 /** 707 * Key of the parameter. 708 */ 709 protected String key; 710 /** 711 * Value of the parameter. 712 */ 713 protected Object value; 714 715 /** 716 * Build a new parameter. 717 * 718 * @param key 719 * @param value 720 */ 721 public Parameter(String key, Object value) { 722 this.key = key; 723 this.value = value; 724 } 725 726 /** 727 * Get the key of this parameter. 728 * 729 * @return the key 730 */ 731 public String getKey() { 732 return key; 733 } 734 735 /** 736 * Get the value of this parameter. 737 * 738 * @param <T> 739 * type asked for the value 740 * @return the value as a T 741 */ 742 @SuppressWarnings("unchecked") 743 public <T> T getValue() { 744 return (T) value; 745 } 746}