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