001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2012 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020
021package org.shredzone.commons.view.manager;
022
023import java.lang.reflect.Method;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import javax.annotation.Nonnull;
032import javax.annotation.Nullable;
033import javax.annotation.ParametersAreNonnullByDefault;
034import javax.annotation.PostConstruct;
035import javax.annotation.Resource;
036
037import org.shredzone.commons.view.Signature;
038import org.shredzone.commons.view.annotation.View;
039import org.shredzone.commons.view.annotation.ViewGroup;
040import org.shredzone.commons.view.annotation.ViewHandler;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043import org.springframework.context.ApplicationContext;
044import org.springframework.core.annotation.AnnotationUtils;
045import org.springframework.core.convert.ConversionService;
046import org.springframework.stereotype.Component;
047import org.springframework.util.StringUtils;
048
049/**
050 * Manages the view handlers.
051 *
052 * @author Richard "Shred" Körber
053 */
054@Component
055@ParametersAreNonnullByDefault
056public class ViewManager {
057    private final Logger log = LoggerFactory.getLogger(this.getClass());
058
059    @Resource private ApplicationContext applicationContext;
060    @Resource private ConversionService conversionService;
061
062    private Map<String, Map<String, List<ViewPattern>>> patternMap = new HashMap<>();
063    private Map<String, Map<Signature, ViewPattern>> signatureMap = new HashMap<>();
064    private List<ViewPattern> patternOrder = new ArrayList<>();
065
066    /**
067     * Returns a collection of all defined {@link ViewPattern}.
068     *
069     * @return Collection of matching {@link ViewPattern}
070     */
071    public @Nonnull Collection<ViewPattern> getViewPatterns() {
072        return Collections.unmodifiableCollection(patternOrder);
073    }
074
075    /**
076     * Returns a collection of {@link ViewPattern} that were defined for the given view.
077     *
078     * @param view
079     *            View name
080     * @param qualifier
081     *            Qualifier name, or {@code null}
082     * @return Collection of matching {@link ViewPattern}, empty if there is no such view
083     */
084    public @Nonnull Collection<ViewPattern> getViewPatternsForView(String view, @Nullable String qualifier) {
085        Map<String, List<ViewPattern>> viewMap = patternMap.get(view);
086        if (viewMap != null) {
087            List<ViewPattern> result = viewMap.get(qualifier);
088            if (result != null) {
089                return Collections.unmodifiableCollection(result);
090            }
091        }
092        return Collections.emptyList();
093    }
094
095    /**
096     * Returns the {@link ViewPattern} that handles the given {@link Signature}.
097     *
098     * @param signature
099     *            {@link Signature} to find a {@link ViewPattern} for
100     * @param qualifier
101     *            Qualifier name, or {@code null}
102     * @return {@link ViewPattern} found, or {@code null} if there is no such
103     *         {@link ViewPattern}
104     */
105    public ViewPattern getViewPatternForSignature(Signature signature, @Nullable String qualifier) {
106        Map<Signature, ViewPattern> sigMap = signatureMap.get(qualifier);
107        if (sigMap != null) {
108            return sigMap.get(signature);
109        }
110        return null;
111    }
112
113    /**
114     * Sets up the view manager. All Spring beans are searched for {@link ViewHandler}
115     * annotations.
116     */
117    @PostConstruct
118    protected void setup() {
119        Collection<Object> beans = applicationContext.getBeansWithAnnotation(ViewHandler.class).values();
120        for (Object bean : beans) {
121            ViewHandler vhAnno = bean.getClass().getAnnotation(ViewHandler.class);
122            if (vhAnno != null) {
123                for (Method method : bean.getClass().getMethods()) {
124                    ViewGroup groupAnno = AnnotationUtils.findAnnotation(method, ViewGroup.class);
125                    if (groupAnno != null) {
126                        for (View viewAnno : groupAnno.value()) {
127                            processView(bean, method, viewAnno);
128                        }
129                    }
130
131                    View viewAnno = AnnotationUtils.findAnnotation(method, View.class);
132                    if (viewAnno != null) {
133                        processView(bean, method, viewAnno);
134                    }
135                }
136            }
137        }
138
139        patternMap.values().forEach(pm -> pm.values().forEach(Collections::sort));
140        Collections.sort(patternOrder);
141    }
142
143    /**
144     * Processes a {@link View}. A view name and view pattern is generated, and a
145     * {@link ViewInvoker} is built.
146     *
147     * @param bean
148     *            Spring bean to be used
149     * @param method
150     *            View handler method to be invoked
151     * @param anno
152     *            {@link View} annotation
153     */
154    private void processView(Object bean, Method method, View anno) {
155        String name = computeViewName(method, anno);
156
157        Map<String, List<ViewPattern>> vpMap = patternMap.computeIfAbsent(name, it -> new HashMap<>());
158
159        ViewInvoker invoker = new ViewInvoker(bean, method, conversionService);
160        ViewPattern vp = new ViewPattern(anno, invoker);
161
162        List<ViewPattern> vpList = vpMap.computeIfAbsent(vp.getQualifier(), it -> new ArrayList<>());
163        vpList.add(vp);
164
165        Signature sig = vp.getSignature();
166        if (sig != null) {
167            Map<Signature, ViewPattern> sigMap = signatureMap.computeIfAbsent(vp.getQualifier(), it -> new HashMap<>());
168            if (sigMap.putIfAbsent(sig, vp) != null) {
169                throw new IllegalStateException("Signature '" + sig + "' defined twice");
170            }
171        }
172
173        patternOrder.add(vp);
174
175        log.info("Found view '{}' with pattern '{}'", name, anno.pattern());
176    }
177
178    /**
179     * Computes a view name. If the {@link View} annotation contains a name, it is used.
180     * If no name is given, it is guessed by the method name. If the method name ends with
181     * "View", it is removed.
182     *
183     * @param method
184     *            {@link Method} of the view handler
185     * @param anno
186     *            {@link View} annotation
187     * @return view name to be used for this view
188     */
189    private @Nonnull String computeViewName(Method method, View anno) {
190        if (StringUtils.hasText(anno.name())) {
191            return anno.name();
192        }
193
194        String name = method.getName();
195        if (name.length() > 4 && name.endsWith("View")) {
196            name = name.substring(0, name.length() - "View".length());
197        }
198
199        return name;
200    }
201
202}