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}