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}