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