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}