001package ball.spring; 002/*- 003 * ########################################################################## 004 * Reusable Spring Components 005 * $Id: AbstractController.java 7215 2021-01-03 18:39:51Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-spring/trunk/jar/src/main/java/ball/spring/AbstractController.java $ 007 * %% 008 * Copyright (C) 2018 - 2021 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import ball.spring.dialect.WebJarsDialect; 024import java.util.Map; 025import java.util.NoSuchElementException; 026import java.util.Properties; 027import java.util.concurrent.ConcurrentSkipListMap; 028import java.util.regex.Pattern; 029import javax.annotation.PostConstruct; 030import javax.annotation.PreDestroy; 031import lombok.NoArgsConstructor; 032import lombok.ToString; 033import lombok.extern.log4j.Log4j2; 034import org.springframework.beans.factory.annotation.Autowired; 035import org.springframework.beans.factory.annotation.Value; 036import org.springframework.beans.factory.config.PropertiesFactoryBean; 037import org.springframework.boot.web.servlet.error.ErrorController; 038import org.springframework.context.ApplicationContext; 039import org.springframework.core.io.Resource; 040import org.springframework.ui.Model; 041import org.springframework.validation.support.BindingAwareModelMap; 042import org.springframework.web.bind.annotation.ExceptionHandler; 043import org.springframework.web.bind.annotation.ModelAttribute; 044import org.springframework.web.bind.annotation.RequestMapping; 045import org.springframework.web.bind.annotation.ResponseBody; 046import org.springframework.web.bind.annotation.ResponseStatus; 047import org.thymeleaf.spring5.SpringTemplateEngine; 048import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; 049import org.webjars.RequireJS; 050 051import static lombok.AccessLevel.PROTECTED; 052import static org.apache.commons.lang3.StringUtils.appendIfMissing; 053import static org.apache.commons.lang3.StringUtils.prependIfMissing; 054import static org.apache.commons.lang3.StringUtils.removeEnd; 055import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; 056import static org.springframework.http.HttpStatus.NOT_FOUND; 057 058/** 059 * Abstract {@link org.springframework.stereotype.Controller} base class. 060 * Implements {@link ErrorController}, implements {@link #getViewName()} 061 * (with 062 * {@code String.join("-", getClass().getPackage().getName().split(Pattern.quote(".")))}), 063 * provides {@link #addDefaultModelAttributesTo(Model)} from corresponding 064 * {@code template.model.properties}, and configures 065 * {@link SpringResourceTemplateResolver} to use decoupled logic. 066 * 067 * {@injected.fields} 068 * 069 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 070 * @version $Revision: 7215 $ 071 */ 072@NoArgsConstructor(access = PROTECTED) @ToString @Log4j2 073public abstract class AbstractController implements ErrorController { 074 @Value("${server.error.path:${error.path:/error}}") 075 private String errorPath = null; 076 077 @Autowired 078 private ApplicationContext context = null; 079 080 @Autowired 081 private SpringTemplateEngine engine = null; 082 083 @Autowired 084 private SpringResourceTemplateResolver resolver = null; 085 086 private ConcurrentSkipListMap<String,Properties> viewDefaultAttributesMap = 087 new ConcurrentSkipListMap<>(); 088 089 @PostConstruct 090 public void init() { 091 engine.addDialect(new WebJarsDialect()); 092 resolver.setUseDecoupledLogic(true); 093 } 094 095 @PreDestroy 096 public void destroy() { } 097 098 /* org.springframework.web.servlet.RequestToViewNameTranslator */ 099 public String getViewName(/* HttpServletRequest request */) { 100 return String.join("-", 101 getClass().getPackage().getName().split(Pattern.quote("."))); 102 } 103 104 @ModelAttribute 105 public void addDefaultModelAttributesTo(Model model) { 106 BindingAwareModelMap defaults = new BindingAwareModelMap(); 107 Properties properties = 108 viewDefaultAttributesMap 109 .computeIfAbsent(getViewName(), k -> getDefaultAttributesFor(k)); 110 111 for (Map.Entry<Object,Object> entry : properties.entrySet()) { 112 String key = entry.getKey().toString(); 113 String value = entry.getValue().toString(); 114 115 while (value != null) { 116 String unresolved = value; 117 118 value = 119 context.getEnvironment().resolvePlaceholders(unresolved); 120 121 if (unresolved.equals(value)) { 122 break; 123 } 124 } 125 126 defaults.put(key, value); 127 } 128 129 model.mergeAttributes(defaults.asMap()); 130 } 131 132 private Properties getDefaultAttributesFor(String name) { 133 Properties properties = null; 134 135 try { 136 name = prependIfMissing(name, resolver.getPrefix()); 137 name = removeEnd(name, resolver.getSuffix()); 138 name = appendIfMissing(name, ".model.properties"); 139 140 properties = 141 new PropertiesFactory(context.getResources(name)) 142 .getObject(); 143 } catch (RuntimeException exception) { 144 throw exception; 145 } catch (Exception exception) { 146 throw new IllegalStateException(exception); 147 } 148 149 return properties; 150 } 151 152 /** 153 * See {@link RequireJS#getSetupJavaScript(String)}. 154 * 155 * @return The set-up javascript. 156 */ 157 @ResponseBody 158 @RequestMapping(value = "/webjarsjs", 159 produces = "application/javascript") 160 public String webjarsjs() { 161 return RequireJS.getSetupJavaScript("/webjars/"); 162 } 163 164 @RequestMapping(value = "${server.error.path:${error.path:/error}}") 165 public String error() { return getViewName(); } 166 167 @ExceptionHandler 168 @ResponseStatus(value = NOT_FOUND) 169 public String handleNOT_FOUND(Model model, 170 NoSuchElementException exception) { 171 return handle(model, exception); 172 } 173 174 @ExceptionHandler 175 @ResponseStatus(value = INTERNAL_SERVER_ERROR) 176 public String handle(Model model, Exception exception) { 177 addDefaultModelAttributesTo(model); 178 179 model.addAttribute("exception", exception); 180 181 return getViewName(); 182 } 183 184 @Deprecated @Override 185 public String getErrorPath() { return errorPath; } 186 187 @ToString 188 private class PropertiesFactory extends PropertiesFactoryBean { 189 public PropertiesFactory(Resource[] resources) { 190 super(); 191 192 try { 193 setIgnoreResourceNotFound(true); 194 setLocations(resources); 195 setSingleton(false); 196 197 mergeProperties(); 198 } catch (Exception exception) { 199 throw new ExceptionInInitializerError(exception); 200 } 201 } 202 } 203}