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.progmodel.facets.object.title.annotation;
021
022import java.lang.reflect.Method;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.List;
026
027import com.google.common.base.Splitter;
028import com.google.common.collect.Lists;
029
030import org.apache.isis.applib.annotation.Title;
031import org.apache.isis.core.commons.config.IsisConfiguration;
032import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
033import org.apache.isis.core.metamodel.adapter.mgr.AdapterManagerAware;
034import org.apache.isis.core.metamodel.facetapi.FacetHolder;
035import org.apache.isis.core.metamodel.facetapi.FacetUtil;
036import org.apache.isis.core.metamodel.facetapi.FeatureType;
037import org.apache.isis.core.metamodel.facetapi.MetaModelValidatorRefiner;
038import org.apache.isis.core.metamodel.facets.FacetFactoryAbstract;
039import org.apache.isis.core.metamodel.methodutils.MethodScope;
040import org.apache.isis.core.metamodel.spec.ObjectSpecification;
041import org.apache.isis.core.metamodel.specloader.validator.MetaModelValidatorComposite;
042import org.apache.isis.core.metamodel.specloader.validator.MetaModelValidatorVisiting;
043import org.apache.isis.core.metamodel.specloader.validator.ValidationFailures;
044import org.apache.isis.core.progmodel.facets.MethodFinderUtils;
045import org.apache.isis.core.progmodel.facets.fallback.FallbackFacetFactory;
046import org.apache.isis.core.progmodel.facets.object.title.TitleMethodFacetFactory;
047import org.apache.isis.core.progmodel.facets.object.title.annotation.TitleFacetViaTitleAnnotation.TitleComponent;
048
049public class TitleAnnotationFacetFactory extends FacetFactoryAbstract implements AdapterManagerAware, MetaModelValidatorRefiner {
050
051    private static final String TITLE_METHOD_NAME = "title";
052
053    private AdapterManager adapterManager;
054
055    public TitleAnnotationFacetFactory() {
056        super(FeatureType.OBJECTS_ONLY);
057    }
058
059    /**
060     * If no method tagged with {@link Title} annotation then will use Facets
061     * provided by {@link FallbackFacetFactory} instead.
062     */
063    @Override
064    public void process(final ProcessClassContext processClassContext) {
065        final Class<?> cls = processClassContext.getCls();
066        final FacetHolder facetHolder = processClassContext.getFacetHolder();
067
068        final List<Method> methods = MethodFinderUtils.findMethodsWithAnnotation(cls, MethodScope.OBJECT, Title.class);
069
070        Collections.sort(methods, new Comparator<Method>() {
071            Comparator<String> comparator = new SequenceComparator();
072
073            @Override
074            public int compare(final Method o1, final Method o2) {
075                final Title a1 = o1.getAnnotation(Title.class);
076                final Title a2 = o2.getAnnotation(Title.class);
077                return comparator.compare(a1.sequence(), a2.sequence());
078            }
079        });
080        if (methods.isEmpty()) {
081            return;
082        }
083        final List<TitleComponent> titleComponents = Lists.transform(methods, TitleComponent.FROM_METHOD);
084        FacetUtil.addFacet(new TitleFacetViaTitleAnnotation(titleComponents, facetHolder, adapterManager));
085    }
086
087
088
089
090    static class SequenceComparator implements Comparator<String> {
091
092        @Override
093        public int compare(final String sequence1, final String sequence2) {
094
095            final List<String> components1 = componentsFor(sequence1);
096            final List<String> components2 = componentsFor(sequence2);
097
098            final int size1 = components1.size();
099            final int size2 = components2.size();
100
101            if (size1 == 0 && size2 == 0) {
102                return 0;
103            }
104
105            // continue to loop until we run out of components.
106            int n = 0;
107            while (true) {
108                final int length = n + 1;
109                // check if run out of components in either side
110                if (size1 < length && size2 >= length) {
111                    return -1; // o1 before o2
112                }
113                if (size2 < length && size1 >= length) {
114                    return +1; // o2 before o1
115                }
116                if (size1 < length && size2 < length) {
117                    // run out of components
118                    return 0;
119                }
120                // we have this component on each side
121                int componentCompare = 0;
122                try {
123                    final Integer c1 = Integer.valueOf(components1.get(n));
124                    final Integer c2 = Integer.valueOf(components2.get(n));
125                    componentCompare = c1.compareTo(c2);
126                } catch (final NumberFormatException nfe) {
127                    // not integers compare as strings
128                    componentCompare = components1.get(n).compareTo(components2.get(n));
129                }
130
131                if (componentCompare != 0) {
132                    return componentCompare;
133                }
134                // this component is the same; lets look at the next
135                n++;
136            }
137        }
138
139        private static List<String> componentsFor(final String sequence) {
140            return Lists.newArrayList(Splitter.on('.').split(sequence));
141        }
142    }
143
144
145    @Override
146    public void setAdapterManager(final AdapterManager adapterMap) {
147        this.adapterManager = adapterMap;
148    }
149
150    /**
151     * Violation if there is a class that has both a <tt>title()</tt> method and also any non-inherited method 
152     * annotated with <tt>@Title</tt>.
153     * 
154     * <p>
155     * If there are only inherited methods annotated with <tt>@Title</tt> then this is <i>not</i> a violation; but 
156     * (from the implementation of {@link TitleMethodFacetFactory} the imperative <tt>title()</tt> method will take
157     * precedence.
158     */
159    @Override
160    public void refineMetaModelValidator(MetaModelValidatorComposite metaModelValidator, IsisConfiguration configuration) {
161        metaModelValidator.add(new MetaModelValidatorVisiting(new MetaModelValidatorVisiting.Visitor() {
162
163            @Override
164            public boolean visit(ObjectSpecification objectSpec, ValidationFailures validationFailures) {
165                final Class<?> cls = objectSpec.getCorrespondingClass();
166
167                final Method titleMethod = MethodFinderUtils.findMethod(cls, MethodScope.OBJECT, TITLE_METHOD_NAME, String.class, null);
168                if (titleMethod == null) {
169                    return true;
170                }
171                
172                // determine if cls contains an @Title annotated method, not inherited from superclass
173                final Class<?> supClass = cls.getSuperclass();
174                if (supClass == null) {
175                    return true;
176                }
177                
178                final List<Method> methods = methodsWithTitleAnnotation(cls);
179                final List<Method> superClassMethods = methodsWithTitleAnnotation(supClass);
180                if (methods.size() > superClassMethods.size()) {
181                    validationFailures.add(
182                            "Conflict for determining a strategy for retrieval of title for class %s "
183                            + "that contains a method '%s' and an annotation '@%s'", 
184                            objectSpec.getIdentifier().getClassName(), TITLE_METHOD_NAME, Title.class.getName());
185                }
186
187                return true;
188            }
189
190            private List<Method> methodsWithTitleAnnotation(final Class<?> cls) {
191                return MethodFinderUtils.findMethodsWithAnnotation(cls, MethodScope.OBJECT, Title.class);
192            }
193
194        }));
195    }
196}