001package ball.spring; 002/*- 003 * ########################################################################## 004 * Reusable Spring Components 005 * %% 006 * Copyright (C) 2018 - 2025 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.apache.commons.lang3.Strings; 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.SpringTemplateEngine; 047import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; 048import org.webjars.RequireJS; 049 050import static lombok.AccessLevel.PROTECTED; 051import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; 052import static org.springframework.http.HttpStatus.NOT_FOUND; 053 054/** 055 * Abstract {@link org.springframework.stereotype.Controller} base class. 056 * Implements {@link ErrorController}, implements {@link #getViewName()} 057 * (with 058 * {@code String.join("-", getClass().getPackage().getName().split(Pattern.quote(".")))}), 059 * provides {@link #addDefaultModelAttributesTo(Model)} from corresponding 060 * {@code template.model.properties}, and configures 061 * {@link SpringResourceTemplateResolver} to use decoupled logic. 062 * 063 * {@injected.fields} 064 * 065 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 066 */ 067@NoArgsConstructor(access = PROTECTED) @ToString @Log4j2 068public abstract class AbstractController implements ErrorController { 069 @Value("${server.error.path:${error.path:/error}}") 070 private String errorPath = null; 071 072 @Autowired 073 private ApplicationContext context = null; 074 075 @Autowired 076 private SpringTemplateEngine engine = null; 077 078 @Autowired 079 private SpringResourceTemplateResolver resolver = null; 080 081 private ConcurrentSkipListMap<String,Properties> viewDefaultAttributesMap = new ConcurrentSkipListMap<>(); 082 083 @PostConstruct 084 public void init() { 085 engine.addDialect(new WebJarsDialect()); 086 resolver.setUseDecoupledLogic(true); 087 } 088 089 @PreDestroy 090 public void destroy() { } 091 092 /* org.springframework.web.servlet.RequestToViewNameTranslator */ 093 public String getViewName(/* HttpServletRequest request */) { 094 return String.join("-", getClass().getPackage().getName().split(Pattern.quote("."))); 095 } 096 097 @ModelAttribute 098 public void addDefaultModelAttributesTo(Model model) { 099 BindingAwareModelMap defaults = new BindingAwareModelMap(); 100 Properties properties = 101 viewDefaultAttributesMap.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 = context.getEnvironment().resolvePlaceholders(unresolved); 111 112 if (unresolved.equals(value)) { 113 break; 114 } 115 } 116 117 defaults.put(key, value); 118 } 119 120 model.mergeAttributes(defaults.asMap()); 121 } 122 123 private Properties getDefaultAttributesFor(String name) { 124 Properties properties = null; 125 126 try { 127 name = Strings.CI.prependIfMissing(name, resolver.getPrefix()); 128 name = Strings.CI.removeEnd(name, resolver.getSuffix()); 129 name = Strings.CI.appendIfMissing(name, ".model.properties"); 130 131 properties = new PropertiesFactory(context.getResources(name)).getObject(); 132 } catch (RuntimeException exception) { 133 throw exception; 134 } catch (Exception exception) { 135 throw new IllegalStateException(exception); 136 } 137 138 return properties; 139 } 140 141 /** 142 * See {@link RequireJS#getSetupJavaScript(String)}. 143 * 144 * @return The set-up javascript. 145 */ 146 @ResponseBody 147 @RequestMapping(value = "/webjarsjs", produces = "application/javascript") 148 public String webjarsjs() { 149 return RequireJS.getSetupJavaScript("/webjars/"); 150 } 151 152 @RequestMapping(value = "${server.error.path:${error.path:/error}}") 153 public String error() { return getViewName(); } 154 155 @ExceptionHandler 156 @ResponseStatus(value = NOT_FOUND) 157 public String handleNOT_FOUND(Model model, NoSuchElementException exception) { 158 return handle(model, exception); 159 } 160 161 @ExceptionHandler 162 @ResponseStatus(value = INTERNAL_SERVER_ERROR) 163 public String handle(Model model, Exception exception) { 164 addDefaultModelAttributesTo(model); 165 166 model.addAttribute("exception", exception); 167 168 return getViewName(); 169 } 170 171 @ToString 172 private class PropertiesFactory extends PropertiesFactoryBean { 173 public PropertiesFactory(Resource[] resources) { 174 super(); 175 176 try { 177 setIgnoreResourceNotFound(true); 178 setLocations(resources); 179 setSingleton(false); 180 181 mergeProperties(); 182 } catch (Exception exception) { 183 throw new ExceptionInInitializerError(exception); 184 } 185 } 186 } 187}