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}