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}