001package org.biopax.paxtools.controller;
002
003import org.apache.commons.logging.Log;
004import org.apache.commons.logging.LogFactory;
005import org.biopax.paxtools.model.BioPAXElement;
006import org.biopax.paxtools.util.IllegalBioPAXArgumentException;
007
008import java.lang.reflect.Method;
009import java.lang.reflect.ParameterizedType;
010import java.lang.reflect.Type;
011import java.util.*;
012
013
014/**
015 * This is the base class for all property editors. Each property controller is responsible for
016 * manipulating a certain property for a given class of objects (domain).
017 */
018public abstract class AbstractPropertyEditor<D extends BioPAXElement, R>
019                extends SimplePropertyAccessor<D,R> implements PropertyEditor<D,R>
020{
021// ------------------------------ FIELDS ------------------------------
022
023        protected static final Log log = LogFactory.getLog(AbstractPropertyEditor.class);
024
025        /**
026         * This variable stores the method to invoke for setting a property to the to the given value. If
027         * {@link #multipleCardinality multiple cardinality}, the returned method is expected to have a
028         * {@link java.util.Set} as its only parameter.
029         */
030        protected Method setMethod;
031
032        /**
033         * This variable stores the method to invoke for adding the given value to the property managed by
034         * this commander. In the case of multiple cardinality, the method is expected to have a {@link
035         * #range} as its only parameter, otherwise expected to be null.
036         */
037        protected Method addMethod;
038
039        /**
040         * This variable stores the method for removing the value of the property on a given bean. In the
041         * case of multiple cardinality, this method is expected to have a {@link #range} as its only
042         * parameter, otherwise expected to be null
043         */
044        protected Method removeMethod;
045
046        /**
047         * Local OWL name of the property
048         */
049        protected final String property;
050
051
052        /**
053         * This map keeps a list of maximum cardinality restrictions.
054         */
055        private final Map<Class, Integer> maxCardinalities = new HashMap<Class, Integer>();
056
057    public static ThreadLocal<Boolean> checkRestrictions = new ThreadLocal<Boolean>()
058    {
059        @Override
060        protected Boolean initialValue()
061        {
062            return true;
063        }
064    };
065
066
067// --------------------------- CONSTRUCTORS ---------------------------
068
069        /**
070         * Constructor.
071         * 
072         * @param property biopax property name
073         * @param getMethod getter
074         * @param domain class the property belongs to
075         * @param range property values type/class
076         * @param multipleCardinality whether more than one value is allowed
077         */
078    public AbstractPropertyEditor(String property, Method getMethod, Class<D> domain, Class<R> range,
079                        boolean multipleCardinality)
080        {
081                super(domain, range, multipleCardinality, getMethod);
082                this.property = property;
083
084                try
085                {
086                        detectMethods();
087                }
088                catch (NoSuchMethodException e)
089                {
090                        log.error("Failed at reflection, no method: " + e.getMessage());
091                }
092        }
093
094
095// -------------------------- STATIC METHODS --------------------------
096
097        @Override
098        public String toString()
099        {
100                String def = String.format("%s %s %s", domain.getSimpleName(), property, range.getSimpleName());
101
102                //TODO cardinalities are not being read!
103                for (Class aClass : maxCardinalities.keySet())
104                {
105                        Integer cardinality = maxCardinalities.get(aClass);
106                        def += " C:" + aClass.getSimpleName() + ":" + cardinality;
107                }
108
109                return def;
110        }
111
112        /**
113         * This method creates a property reflecting on the domain and property. Proper subclass is chosen
114         * based on the range of the property.
115         * @param domain paxtools level2 interface that maps to the corresponding owl level2.
116         * @param property to be managed by the constructed controller.
117         * @param <D> domain
118         * @param <R> range
119         * @return a property controller to manipulate the beans for the given property.
120         */
121        public static <D extends BioPAXElement, R> PropertyEditor<D, R> createPropertyEditor(Class<D> domain,
122                                                                                             String property)
123        {
124                PropertyEditor editor = null;
125                try
126                {
127                        Method getMethod = detectGetMethod(domain, property);
128                        boolean multipleCardinality = isMultipleCardinality(getMethod);
129                        Class<R> range = detectRange(getMethod);
130
131                        if (range.isPrimitive() || range.equals(Boolean.class))
132                        {
133                                editor = new PrimitivePropertyEditor<D, R>(property, getMethod, domain, range, multipleCardinality);
134                        } else if (range.isEnum())
135                        {
136                                editor = new EnumeratedPropertyEditor(property, getMethod, domain, range, multipleCardinality);
137                        } else if (range.equals(String.class))
138                        {
139                                editor = new StringPropertyEditor(property, getMethod, domain, multipleCardinality);
140                        } else
141                        {
142                                editor = new ObjectPropertyEditor(property, getMethod, domain, range, multipleCardinality);
143                        }
144                }
145                catch (NoSuchMethodException e)
146                {
147                        if (log.isWarnEnabled()) log.warn("Failed creating the controller for " + property + " on " + domain);
148                }
149                return editor;
150        }
151
152        private static Method detectGetMethod(Class beanClass, String property) throws NoSuchMethodException
153        {
154                String javaMethodName = getJavaName(property);
155                //This is the name we are going to try, log it down
156                if (log.isTraceEnabled())
157                {
158                        log.trace("javaMethodName = get" + javaMethodName);
159                }
160
161                //extract the get method
162                return beanClass.getMethod("get" + javaMethodName);
163        }
164
165        /**
166         * Given the name of a property's name as indicated in the OWL file, this method converts the name
167         * to a Java compatible name.
168         * @param owlName the property name as a string
169         * @return the Java compatible name of the property
170         */
171        private static String getJavaName(String owlName)
172        {
173                // Since java does not allow '-' replace them all with '_'
174                String s = owlName.replaceAll("-", "_");
175                s = s.substring(0, 1).toUpperCase() + s.substring(1);
176                return s;
177        }
178
179        /**
180         * Given the multiple cardinality feature, the range of the get method is returned.
181         * @param getMethod default method
182
183         * @return the range as a class
184         */
185        protected static Class detectRange(Method getMethod)
186        {
187                Class range = getMethod.getReturnType();
188                //if the return type is a collection then we have multiple cardinality
189                if (Collection.class.isAssignableFrom(range))
190                {
191                        //it is a collection, by default assume non parameterized.
192                        range = Object.class;
193                        //If the collection is parameterized, get it.
194                        Type genericReturnType = getMethod.getGenericReturnType();
195                        if (genericReturnType instanceof ParameterizedType)
196                        {
197                                try
198                                {
199                                        range = (Class) ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
200                                }
201                                catch (Exception e)
202                                {
203                                        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
204                                }
205                                //Now this is required as autoboxing will not work with reflection
206                                if (range == Double.class)
207                                {
208                                        range = double.class;
209                                }
210                                if (range == Float.class)
211                                {
212                                        range = float.class;
213                                }
214                                if (range == Integer.class)
215                                {
216                                        range = int.class;
217                                }
218                        }
219                        if (log.isTraceEnabled())
220                        {
221                                log.trace(range);
222                        }
223                }
224
225                return range;
226        }
227
228
229        /**
230         * Detects and sets the default methods for the property to which editor is associated. If property
231         * has multiple cardinality, {@link #setMethod}, {@link #addMethod}, and {@link #removeMethod} are
232         * set, otherwise only the {@link #setMethod}.
233         * @exception NoSuchMethodException if a method for the property does not exist
234         */
235        private void detectMethods() throws NoSuchMethodException
236        {
237                String javaName = getJavaName(property);
238                if (multipleCardinality)
239                {
240
241                        this.addMethod = domain.getMethod("add" + javaName, range);
242                        this.removeMethod = domain.getMethod("remove" + javaName, range);
243                } else
244                {
245                        this.setMethod = domain.getMethod("set" + javaName, range);
246                }
247        }
248
249        // --------------------- GETTER / SETTER METHODS ---------------------
250
251        @Override public Method getAddMethod()
252        {
253                return addMethod;
254        }
255
256        @Override public Method getGetMethod()
257        {
258                return getMethod;
259        }
260
261        @Override public String getProperty()
262        {
263                return property;
264        }
265
266        @Override public Method getRemoveMethod()
267        {
268                return removeMethod;
269        }
270
271        @Override public Method getSetMethod()
272        {
273                return setMethod;
274        }
275
276// --------------------- ACCESORS and MUTATORS---------------------
277
278        // -------------------------- OTHER METHODS --------------------------
279
280        @Override public void addMaxCardinalityRestriction(Class<? extends D> domain, int max)
281        {
282                if (multipleCardinality)
283                {
284                        this.maxCardinalities.put(domain, max);
285                } else
286                {
287                        if (max == 1)
288                        {
289                                if (log.isInfoEnabled())
290                                {
291                                        log.info("unnecessary use of cardinality restriction. " +
292                                                 "Maybe you want to use functional instead?");
293                                }
294                        } else if (max == 0)
295                        {
296                                this.maxCardinalities.put(domain, max);
297                        } else
298                        {
299                                assert false;
300                        }
301                }
302        }
303
304        @Override public Integer getMaxCardinality(Class<? extends D> restrictedDomain)
305        {
306                return this.maxCardinalities.get(restrictedDomain);
307        }
308
309        /**
310         * Checks if <em>value</em> is an instance of one of the classes given in a set. This method
311         * becomes useful, when the restrictions have to be checked for a set of objects. e.g. check if the
312         * value is in the range of the editor.
313         *
314         * @param classes a set of classes to be checked
315         * @param value value whose class will be checked
316         * @return true if value belongs to one of the classes in the set
317         */
318        protected boolean isInstanceOfAtLeastOne(Set<Class<? extends BioPAXElement>> classes, Object value)
319        {
320                boolean check = false;
321                for (Class aClass : classes)
322                {
323                        if (aClass.isInstance(value))
324                        {
325                                check = true;
326                                break;
327                        }
328                }
329                return check;
330        }
331
332
333
334        @Override public R getUnknown()
335        {
336                return null;
337        }
338
339
340        @Override public void removeValueFromBean(R value, D bean)
341        {
342                if(value == null)
343                        return;
344
345                try
346                {
347                        if (removeMethod != null)
348                        {
349                                invokeMethod(removeMethod, bean, value);
350                        } else {
351                                assert !isMultipleCardinality() : "removeMethod is not defined " +
352                                                "for the multiple cardinality property: " + property +
353                                                ". Here, this might add 'unknown' value while keeping exisiting one as well!";
354                                
355                                if(this.getValueFromBean(bean).contains(value))
356                                {
357                                        this.setValueToBean(this.getUnknown(),bean);
358                                }
359                                else { 
360                                        //TODO throw an exception if range is violated rather than always log the following message
361                                        log.error("Given value :" + value + 
362                                                " is not equal to the existing value. " +
363                                                 "remove value is ignored");
364                                        assert getRange().isInstance(value) : "Range violation!";
365                                        assert getDomain().isInstance(bean) : "Domain violation!";
366                                }
367                        }
368                }
369                catch (Exception e)
370                {
371                        log.error(e);
372                }
373        }
374
375
376        @Override public void removeValueFromBean(Set<R> values, D bean) {
377                for(R r : values) {
378                        removeValueFromBean(r, bean);
379                }
380        }
381        
382        
383        /**
384         * Calls the <em>method</em> onto <em>bean</em> with the <em>value</em> as its parameter. In this
385         * context <em>method</em> can be one of these three: set, add, or remove.
386         * @param method method that is going to be called
387         * @param bean bean onto which the method is going to be applied
388         * @param value the value which is going to be used by method
389         */
390        protected void invokeMethod(Method method, D bean, R value)
391        {
392                assert bean != null;
393                try
394                {
395                        method.invoke(domain.cast(bean), value);
396                }
397                catch (ClassCastException e)
398                {
399                        String message = "Failed to set property: " + property;
400                        if (!domain.isAssignableFrom(bean.getClass()))
401                        {
402                                message += "  Invalid domain bean: " + domain.getSimpleName() + " is not assignable from " +
403                                           bean.getClass();
404                        }
405                        if (!range.isAssignableFrom(value.getClass()))
406                        {
407                                message += " Invalid range value: " + range + " is not assignable from " + value.getClass();
408                        }
409                        throw new IllegalBioPAXArgumentException(message, e);
410                }
411                catch (Exception e) //java.lang.reflect.InvocationTargetException
412                {
413                        String valInfo = (value == null) ? null : value.getClass().getSimpleName() + ", " + value;
414                        String message = "Failed to set " + property + " with " + method.getName() + " on " 
415                                + domain.getSimpleName() + " (" + bean.getClass().getSimpleName() + ", " + bean + ")" 
416                                + " with range: " + range.getSimpleName() + " (" + valInfo + ")";
417                        throw new IllegalBioPAXArgumentException(message, e);
418                        //TODO actual exceptions thrown by the biopax setter/method are lost, unfortunately...
419                }
420        }
421
422        protected R parseValueFromString(String value)
423        {
424                throw new IllegalBioPAXArgumentException();
425        }
426
427        @Override public void setValueToBean(R value, D bean)
428        {
429                if (this.getPrimarySetMethod() != null)
430                {
431                        if (log.isTraceEnabled()) log.trace(
432                                        this.getPrimarySetMethod().getName() + " bean:" + bean + " val:" + value);
433                } else
434                {
435                        log.error("setMethod is null; " + " bean:" + bean + " (" + bean.getRDFId() + ") val:" + value);
436                }
437
438                // 'null' definitely means 'unknown'for single cardinality props
439                if (value == null && !isMultipleCardinality())
440                        value = getUnknown(); // not null for primitive property editors
441
442                if (value instanceof String)
443                {
444                        value = this.parseValueFromString(((String) value));
445                }
446                try
447                {
448                        if (value != null && checkRestrictions.get()) checkRestrictions(value, bean);
449                        invokeMethod(this.getPrimarySetMethod(), bean, value);
450                }
451                catch (Exception e)
452                {
453                        log.error("Failed to set value: " + value + " to bean " + bean 
454                                        + "; bean class: " + bean.getClass().getSimpleName() 
455                                        + "; primary set method: " + getPrimarySetMethod() 
456                                + ((value != null) ? "; value class: " + value.getClass().getSimpleName() : "") 
457                                + ". Error: " + e + ", Cause: " + e.getCause());
458                }
459        }
460
461        @Override public void setValueToBean(Set<R> values, D bean)
462        {
463                if (values == null) setValueToBean(((R) null), bean);
464
465                else if (this.isMultipleCardinality() || values.size() < 2)
466                {
467                        for (R r : values)
468                        {
469                                this.setValueToBean(r, bean);
470                        }
471                } else throw new IllegalBioPAXArgumentException(
472                                        this.getProperty() + " is single cardinality. Can not set" + "it with a set of size larger than" +
473                                        " 1");
474        }
475
476
477        /**
478         * Checks if the <em>bean</em> and the <em>value</em> are consistent with the cardinality rules of
479         * the model. This method is important for validations.
480         * @param value Value that is related to the object
481         * @param bean Object that is related to the value
482         */
483        protected void checkRestrictions(R value, D bean)
484        {
485                Integer max = this.maxCardinalities.get(value.getClass());
486                if (max != null)
487                {
488                        if (max == 0)
489                        {
490                                throw new IllegalBioPAXArgumentException("Cardinality 0 restriction violated");
491                        } else
492                        {
493                                assert multipleCardinality;
494                                Set values = this.getValueFromBean(bean);
495                                if (values.size() >= max)
496                                {
497                                        throw new IllegalBioPAXArgumentException("Cardinality " + max + " restriction violated");
498                                }
499                        }
500                }
501        }
502
503        @Override public Method getPrimarySetMethod()
504        {
505                return multipleCardinality ? addMethod : setMethod;
506        }
507
508
509}