001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.isis.core.metamodel.specloader.specimpl; 021 022import java.lang.reflect.Method; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Properties; 029import java.util.Set; 030 031import com.google.common.collect.Lists; 032import com.google.gson.JsonSyntaxException; 033 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import org.apache.isis.core.commons.config.IsisConfiguration; 038import org.apache.isis.core.commons.exceptions.IsisException; 039import org.apache.isis.core.commons.lang.ListExtensions; 040import org.apache.isis.core.commons.lang.MethodUtil; 041import org.apache.isis.core.commons.util.ToString; 042import org.apache.isis.core.metamodel.exceptions.MetaModelException; 043import org.apache.isis.core.metamodel.facetapi.FacetHolder; 044import org.apache.isis.core.metamodel.facetapi.FeatureType; 045import org.apache.isis.core.metamodel.facetapi.MethodRemover; 046import org.apache.isis.core.metamodel.facets.FacetFactory; 047import org.apache.isis.core.metamodel.facets.FacetFactory.ProcessClassContext; 048import org.apache.isis.core.metamodel.facets.FacetedMethod; 049import org.apache.isis.core.metamodel.facets.FacetedMethodParameter; 050import org.apache.isis.core.metamodel.facets.object.facets.FacetsFacet; 051import org.apache.isis.core.metamodel.facets.typeof.TypeOfFacet; 052import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadata; 053import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadataReader; 054import org.apache.isis.core.metamodel.layoutmetadata.LayoutMetadataReader.ReaderException; 055import org.apache.isis.core.metamodel.layoutmetadata.json.LayoutMetadataReaderFromJson; 056import org.apache.isis.core.metamodel.methodutils.MethodScope; 057import org.apache.isis.core.metamodel.spec.ObjectSpecification; 058import org.apache.isis.core.metamodel.spec.SpecificationLoaderSpi; 059import org.apache.isis.core.metamodel.specloader.classsubstitutor.ClassSubstitutor; 060import org.apache.isis.core.metamodel.specloader.facetprocessor.FacetProcessor; 061import org.apache.isis.core.metamodel.specloader.traverser.SpecificationTraverser; 062 063public class FacetedMethodsBuilder { 064 065 private static final Logger LOG = LoggerFactory.getLogger(FacetedMethodsBuilder.class); 066 067 private static final String GET_PREFIX = "get"; 068 private static final String IS_PREFIX = "is"; 069 070 private static final class FacetedMethodsMethodRemover implements MethodRemover { 071 072 private final List<Method> methods; 073 074 private FacetedMethodsMethodRemover(final List<Method> methods) { 075 this.methods = methods; 076 } 077 078 @Override 079 public void removeMethod(final MethodScope methodScope, final String methodName, final Class<?> returnType, final Class<?>[] parameterTypes) { 080 MethodUtil.removeMethod(methods, methodScope, methodName, returnType, parameterTypes); 081 } 082 083 @Override 084 public List<Method> removeMethods(final MethodScope methodScope, final String prefix, final Class<?> returnType, final boolean canBeVoid, final int paramCount) { 085 return MethodUtil.removeMethods(methods, methodScope, prefix, returnType, canBeVoid, paramCount); 086 } 087 088 @Override 089 public void removeMethod(final Method method) { 090 if (method == null) { 091 return; 092 } 093 for (int i = 0; i < methods.size(); i++) { 094 if (methods.get(i) == null) { 095 continue; 096 } 097 if (methods.get(i).equals(method)) { 098 methods.set(i, null); 099 } 100 } 101 } 102 103 @Override 104 public void removeMethods(final List<Method> methodsToRemove) { 105 for (int i = 0; i < methods.size(); i++) { 106 if (methods.get(i) == null) { 107 continue; 108 } 109 for (final Method method : methodsToRemove) { 110 if (methods.get(i).equals(method)) { 111 methods.set(i, null); 112 break; 113 } 114 } 115 } 116 } 117 } 118 119 private final FacetHolder spec; 120 121 private final Class<?> introspectedClass; 122 private final List<Method> methods; 123 124 private List<FacetedMethod> associationFacetMethods; 125 private List<FacetedMethod> actionFacetedMethods; 126 127 private final FacetedMethodsMethodRemover methodRemover; 128 129 private final FacetProcessor facetProcessor; 130 131 private final SpecificationTraverser specificationTraverser; 132 133 private final ClassSubstitutor classSubstitutor; 134 135 private final SpecificationLoaderSpi specificationLoader; 136 137 // //////////////////////////////////////////////////////////////////////////// 138 // Constructor & finalize 139 // //////////////////////////////////////////////////////////////////////////// 140 141 public FacetedMethodsBuilder(final ObjectSpecificationAbstract spec, final FacetedMethodsBuilderContext facetedMethodsBuilderContext) { 142 if (LOG.isDebugEnabled()) { 143 LOG.debug("creating JavaIntrospector for " + spec.getFullIdentifier()); 144 } 145 146 this.spec = spec; 147 this.introspectedClass = spec.getCorrespondingClass(); 148 this.methods = Arrays.asList(introspectedClass.getMethods()); 149 150 this.methodRemover = new FacetedMethodsMethodRemover(methods); 151 152 this.facetProcessor = facetedMethodsBuilderContext.facetProcessor; 153 this.specificationTraverser = facetedMethodsBuilderContext.specificationTraverser; 154 this.classSubstitutor = facetedMethodsBuilderContext.classSubstitutor; 155 this.specificationLoader = facetedMethodsBuilderContext.specificationLoader; 156 } 157 158 @Override 159 protected void finalize() throws Throwable { 160 super.finalize(); 161 if (LOG.isDebugEnabled()) { 162 LOG.debug("finalizing inspector " + this); 163 } 164 } 165 166 // //////////////////////////////////////////////////////////////////////////// 167 // Class and stuff immediately derived from class 168 // //////////////////////////////////////////////////////////////////////////// 169 170 private String getClassName() { 171 return introspectedClass.getName(); 172 } 173 174 // //////////////////////////////////////////////////////////////////////////// 175 // introspect class 176 // //////////////////////////////////////////////////////////////////////////// 177 178 179 public Properties introspectClass() { 180 LOG.info("introspecting " + getClassName()); 181 if (LOG.isDebugEnabled()) { 182 LOG.debug("introspecting " + getClassName() + ": class-level details"); 183 } 184 185 // process facets at object level 186 // this will also remove some methods, such as the superclass methods. 187 188 final Properties metadataProperties = readMetadataProperties(introspectedClass); 189 190 getFacetProcessor().process(introspectedClass, metadataProperties, methodRemover, spec); 191 192 // if this class has additional facets (as per @Facets), then process 193 // them. 194 final FacetsFacet facetsFacet = spec.getFacet(FacetsFacet.class); 195 if (facetsFacet != null) { 196 final Class<? extends FacetFactory>[] facetFactories = facetsFacet.facetFactories(); 197 for (final Class<? extends FacetFactory> facetFactorie : facetFactories) { 198 FacetFactory facetFactory = null; 199 try { 200 facetFactory = facetFactorie.newInstance(); 201 } catch (final InstantiationException e) { 202 throw new IsisException(e); 203 } catch (final IllegalAccessException e) { 204 throw new IsisException(e); 205 } 206 getFacetProcessor().injectDependenciesInto(facetFactory); 207 facetFactory.process(new ProcessClassContext(introspectedClass, metadataProperties, methodRemover, spec)); 208 } 209 } 210 return metadataProperties; 211 } 212 213 /** 214 * In the future expect this to be configurable (eg read implementations from {@link IsisConfiguration}). 215 * 216 * <p> 217 * Not doing for now, though, because expect the {@link LayoutMetadata} to evolve a bit yet. 218 */ 219 private Properties readMetadataProperties(Class<?> domainClass) { 220 List<LayoutMetadataReader> layoutMetadataReaders = 221 Lists.<LayoutMetadataReader>newArrayList(new LayoutMetadataReaderFromJson(), new LayoutMetadataReaderFromJson()); 222 for (final LayoutMetadataReader reader : layoutMetadataReaders) { 223 try { 224 Properties properties = reader.asProperties(domainClass); 225 if(properties != null) { 226 return properties; 227 } 228 } catch(ReaderException ex) { 229 final String message = reader.toString() +": unable to load layout metadata for " + domainClass.getName() + " (" + ex.getMessage() + ")"; 230 if(ex.getCause() instanceof JsonSyntaxException) { 231 LOG.warn(message); 232 } else { 233 LOG.debug(message); 234 } 235 } 236 } 237 return null; 238 } 239 240 // //////////////////////////////////////////////////////////////////////////// 241 // introspect associations 242 // //////////////////////////////////////////////////////////////////////////// 243 244 /** 245 * Returns a {@link List} of {@link FacetedMethod}s representing object 246 * actions, lazily creating them first if required. 247 */ 248 public List<FacetedMethod> getAssociationFacetedMethods(Properties properties) { 249 if (associationFacetMethods == null) { 250 associationFacetMethods = createAssociationFacetedMethods(properties); 251 } 252 return associationFacetMethods; 253 } 254 255 private List<FacetedMethod> createAssociationFacetedMethods(Properties properties) { 256 if (LOG.isDebugEnabled()) { 257 LOG.debug("introspecting " + getClassName() + ": properties and collections"); 258 } 259 final Set<Method> associationCandidateMethods = getFacetProcessor().findAssociationCandidateAccessors(methods, new HashSet<Method>()); 260 261 // Ensure all return types are known 262 final List<Class<?>> typesToLoad = Lists.newArrayList(); 263 for (final Method method : associationCandidateMethods) { 264 getSpecificationTraverser().traverseTypes(method, typesToLoad); 265 } 266 getSpecificationLoader().loadSpecifications(typesToLoad, introspectedClass); 267 268 // now create FacetedMethods for collections and for properties 269 final List<FacetedMethod> associationFacetedMethods = Lists.newArrayList(); 270 271 findAndRemoveCollectionAccessorsAndCreateCorrespondingFacetedMethods(associationFacetedMethods, properties); 272 findAndRemovePropertyAccessorsAndCreateCorrespondingFacetedMethods(associationFacetedMethods, properties); 273 274 return Collections.unmodifiableList(associationFacetedMethods); 275 } 276 277 private void findAndRemoveCollectionAccessorsAndCreateCorrespondingFacetedMethods(final List<FacetedMethod> associationPeers, Properties properties) { 278 final List<Method> collectionAccessors = Lists.newArrayList(); 279 getFacetProcessor().findAndRemoveCollectionAccessors(methodRemover, collectionAccessors); 280 createCollectionFacetedMethodsFromAccessors(collectionAccessors, associationPeers, properties); 281 } 282 283 /** 284 * Since the value properties and collections have already been processed, 285 * this will pick up the remaining reference properties. 286 * @param properties TODO 287 */ 288 private void findAndRemovePropertyAccessorsAndCreateCorrespondingFacetedMethods(final List<FacetedMethod> fields, Properties properties) { 289 final List<Method> propertyAccessors = Lists.newArrayList(); 290 getFacetProcessor().findAndRemovePropertyAccessors(methodRemover, propertyAccessors); 291 292 findAndRemovePrefixedNonVoidMethods(MethodScope.OBJECT, GET_PREFIX, Object.class, 0, propertyAccessors); 293 findAndRemovePrefixedNonVoidMethods(MethodScope.OBJECT, IS_PREFIX, Boolean.class, 0, propertyAccessors); 294 295 createPropertyFacetedMethodsFromAccessors(propertyAccessors, fields, properties); 296 } 297 298 private void createCollectionFacetedMethodsFromAccessors(final List<Method> accessorMethods, final List<FacetedMethod> facetMethodsToAppendto, Properties properties) { 299 for (final Method accessorMethod : accessorMethods) { 300 if (LOG.isDebugEnabled()) { 301 LOG.debug(" identified accessor method representing collection: " + accessorMethod); 302 } 303 304 // create property and add facets 305 final FacetedMethod facetedMethod = FacetedMethod.createForCollection(introspectedClass, accessorMethod); 306 getFacetProcessor().process(introspectedClass, accessorMethod, methodRemover, facetedMethod, FeatureType.COLLECTION, properties); 307 308 // figure out what the type is 309 Class<?> elementType = Object.class; 310 final TypeOfFacet typeOfFacet = facetedMethod.getFacet(TypeOfFacet.class); 311 if (typeOfFacet != null) { 312 elementType = typeOfFacet.value(); 313 } 314 facetedMethod.setType(elementType); 315 316 // skip if class substitutor says so. 317 if (getClassSubstitutor().getClass(elementType) == null) { 318 continue; 319 } 320 321 facetMethodsToAppendto.add(facetedMethod); 322 } 323 } 324 325 private void createPropertyFacetedMethodsFromAccessors(final List<Method> accessorMethods, final List<FacetedMethod> facetedMethodsToAppendto, Properties properties) throws MetaModelException { 326 327 for (final Method accessorMethod : accessorMethods) { 328 LOG.debug(" identified accessor method representing property: " + accessorMethod); 329 330 final Class<?> returnType = accessorMethod.getReturnType(); 331 332 // skip if class strategy says so. 333 if (getClassSubstitutor().getClass(returnType) == null) { 334 continue; 335 } 336 337 // create a 1:1 association peer 338 final FacetedMethod facetedMethod = FacetedMethod.createForProperty(introspectedClass, accessorMethod); 339 340 // process facets for the 1:1 association 341 getFacetProcessor().process(introspectedClass, accessorMethod, methodRemover, facetedMethod, FeatureType.PROPERTY, properties); 342 343 facetedMethodsToAppendto.add(facetedMethod); 344 } 345 } 346 347 // //////////////////////////////////////////////////////////////////////////// 348 // introspect actions 349 // //////////////////////////////////////////////////////////////////////////// 350 351 /** 352 * Returns a {@link List} of {@link FacetedMethod}s representing object 353 * actions, lazily creating them first if required. 354 */ 355 public List<FacetedMethod> getActionFacetedMethods(final Properties metadataProperties) { 356 if (actionFacetedMethods == null) { 357 actionFacetedMethods = findActionFacetedMethods(MethodScope.OBJECT, metadataProperties); 358 } 359 return actionFacetedMethods; 360 } 361 362 private enum RecognisedHelpersStrategy { 363 SKIP, DONT_SKIP; 364 public boolean skip() { 365 return this == SKIP; 366 } 367 } 368 369 /** 370 * REVIEW: I'm not sure why we do two passes here. 371 * 372 * <p> 373 * Perhaps it's important to skip helpers first. I doubt it, though. 374 */ 375 private List<FacetedMethod> findActionFacetedMethods( 376 final MethodScope methodScope, 377 final Properties metadataProperties) { 378 if (LOG.isDebugEnabled()) { 379 LOG.debug("introspecting " + getClassName() + ": actions"); 380 } 381 final List<FacetedMethod> actionFacetedMethods1 = findActionFacetedMethods(methodScope, RecognisedHelpersStrategy.SKIP, metadataProperties); 382 final List<FacetedMethod> actionFacetedMethods2 = findActionFacetedMethods(methodScope, RecognisedHelpersStrategy.DONT_SKIP, metadataProperties); 383 return ListExtensions.combineWith(actionFacetedMethods1, actionFacetedMethods2); 384 } 385 386 private List<FacetedMethod> findActionFacetedMethods( 387 final MethodScope methodScope, 388 final RecognisedHelpersStrategy recognisedHelpersStrategy, 389 final Properties metadataProperties) { 390 391 if (LOG.isDebugEnabled()) { 392 LOG.debug(" looking for action methods"); 393 } 394 395 final List<FacetedMethod> actionFacetedMethods = Lists.newArrayList(); 396 397 for (int i = 0; i < methods.size(); i++) { 398 final Method method = methods.get(i); 399 if (method == null) { 400 continue; 401 } 402 final FacetedMethod actionPeer = findActionFacetedMethod(methodScope, recognisedHelpersStrategy, method, metadataProperties); 403 if (actionPeer != null) { 404 methods.set(i, null); 405 actionFacetedMethods.add(actionPeer); 406 } 407 } 408 409 return actionFacetedMethods; 410 } 411 412 private FacetedMethod findActionFacetedMethod( 413 final MethodScope methodScope, 414 final RecognisedHelpersStrategy recognisedHelpersStrategy, 415 final Method actionMethod, 416 final Properties metadataProperties) { 417 418 if (!representsAction(actionMethod, methodScope, recognisedHelpersStrategy)) { 419 return null; 420 } 421 422 // build action 423 return createActionFacetedMethod(actionMethod, metadataProperties); 424 } 425 426 private FacetedMethod createActionFacetedMethod( 427 final Method actionMethod, 428 final Properties metadataProperties) { 429 430 if (!isAllParamTypesValid(actionMethod)) { 431 return null; 432 } 433 434 final FacetedMethod action = FacetedMethod.createForAction(introspectedClass, actionMethod); 435 436 // process facets on the action & parameters 437 getFacetProcessor().process(introspectedClass, actionMethod, methodRemover, action, FeatureType.ACTION, metadataProperties); 438 439 final List<FacetedMethodParameter> actionParams = action.getParameters(); 440 for (int j = 0; j < actionParams.size(); j++) { 441 getFacetProcessor().processParams(actionMethod, j, actionParams.get(j)); 442 } 443 444 return action; 445 } 446 447 private boolean isAllParamTypesValid(final Method actionMethod) { 448 for (final Class<?> paramType : actionMethod.getParameterTypes()) { 449 final ObjectSpecification paramSpec = getSpecificationLoader().loadSpecification(paramType); 450 if (paramSpec == null) { 451 return false; 452 } 453 } 454 return true; 455 } 456 457 private boolean representsAction( 458 final Method actionMethod, 459 final MethodScope methodScope, 460 final RecognisedHelpersStrategy recognisedHelpersStrategy) { 461 462 if (!MethodUtil.inScope(actionMethod, methodScope)) { 463 return false; 464 } 465 466 final List<Class<?>> typesToLoad = new ArrayList<Class<?>>(); 467 getSpecificationTraverser().traverseTypes(actionMethod, typesToLoad); 468 469 final boolean anyLoadedAsNull = getSpecificationLoader().loadSpecifications(typesToLoad); 470 if (anyLoadedAsNull) { 471 return false; 472 } 473 474 if (!loadParamSpecs(actionMethod)) { 475 return false; 476 } 477 478 if (getFacetProcessor().recognizes(actionMethod)) { 479 // a bit of a hack 480 if (actionMethod.getName().startsWith("set")) { 481 return false; 482 } 483 if (recognisedHelpersStrategy.skip()) { 484 LOG.info(" skipping possible helper method " + actionMethod); 485 return false; 486 } 487 } 488 489 if (LOG.isDebugEnabled()) { 490 LOG.debug(" identified action " + actionMethod); 491 } 492 493 return true; 494 } 495 496 private boolean loadParamSpecs(final Method actionMethod) { 497 final Class<?>[] parameterTypes = actionMethod.getParameterTypes(); 498 return loadParamSpecs(parameterTypes); 499 } 500 501 private boolean loadParamSpecs(final Class<?>[] parameterTypes) { 502 final int numParameters = parameterTypes.length; 503 for (int j = 0; j < numParameters; j++) { 504 final ObjectSpecification paramSpec = getSpecificationLoader().loadSpecification(parameterTypes[j]); 505 if (paramSpec == null) { 506 return false; 507 } 508 } 509 return true; 510 } 511 512 513 // //////////////////////////////////////////////////////////////////////////// 514 // introspect class post processing 515 // //////////////////////////////////////////////////////////////////////////// 516 517 public void introspectClassPostProcessing(final Properties metadataProperties) { 518 if (LOG.isDebugEnabled()) { 519 LOG.debug("introspecting " + getClassName() + ": class-level post-processing"); 520 } 521 522 getFacetProcessor().processPost(introspectedClass, metadataProperties, methodRemover, spec); 523 } 524 525 // //////////////////////////////////////////////////////////////////////////// 526 // Helpers for finding and removing methods. 527 // //////////////////////////////////////////////////////////////////////////// 528 529 /** 530 * As per 531 * {@link #findAndRemovePrefixedNonVoidMethods(boolean, String, Class, int)} 532 * , but appends to provided {@link List} (collecting parameter pattern). 533 */ 534 private void findAndRemovePrefixedNonVoidMethods( 535 final MethodScope methodScope, 536 final String prefix, 537 final Class<?> returnType, 538 final int paramCount, 539 final List<Method> methodListToAppendTo) { 540 final List<Method> matchingMethods = findAndRemovePrefixedMethods(methodScope, prefix, returnType, false, paramCount); 541 methodListToAppendTo.addAll(matchingMethods); 542 } 543 544 /** 545 * Searches for all methods matching the prefix and returns them, also 546 * removing it from the {@link #methods array of methods} if found. 547 * 548 * @param objectFactory 549 * 550 * @see MethodUtil#removeMethods(Method[], boolean, String, Class, 551 * boolean, int, ClassSubstitutor) 552 */ 553 private List<Method> findAndRemovePrefixedMethods( 554 final MethodScope methodScope, 555 final String prefix, 556 final Class<?> returnType, 557 final boolean canBeVoid, 558 final int paramCount) { 559 return MethodUtil.removeMethods(methods, methodScope, prefix, returnType, canBeVoid, paramCount); 560 } 561 562 // //////////////////////////////////////////////////////////////////////////// 563 // toString 564 // //////////////////////////////////////////////////////////////////////////// 565 566 @Override 567 public String toString() { 568 final ToString str = new ToString(this); 569 str.append("class", getClassName()); 570 return str.toString(); 571 } 572 573 // //////////////////////////////////////////////////////////////////////////// 574 // Dependencies 575 // //////////////////////////////////////////////////////////////////////////// 576 577 private SpecificationLoaderSpi getSpecificationLoader() { 578 return specificationLoader; 579 } 580 581 private SpecificationTraverser getSpecificationTraverser() { 582 return specificationTraverser; 583 } 584 585 private FacetProcessor getFacetProcessor() { 586 return facetProcessor; 587 } 588 589 private ClassSubstitutor getClassSubstitutor() { 590 return classSubstitutor; 591 } 592 593}