diff --git a/pom.xml b/pom.xml index 21f58a2..ea76431 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,8 @@ 1.6 ${encoding} ${encoding} - 3.2.6.RELEASE + 4.3.4.RELEASE + 1.4.2.RELEASE @@ -118,6 +119,18 @@ ${spring.version} true + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + true + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + provided + javax.servlet javax.servlet-api @@ -128,7 +141,7 @@ junit junit - 4.11 + 4.12 test @@ -155,6 +168,12 @@ ${spring.version} test + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + test + cglib cglib diff --git a/src/main/java/org/mixer2/spring/boot/Mixer2AutoConfiguration.java b/src/main/java/org/mixer2/spring/boot/Mixer2AutoConfiguration.java new file mode 100644 index 0000000..4416563 --- /dev/null +++ b/src/main/java/org/mixer2/spring/boot/Mixer2AutoConfiguration.java @@ -0,0 +1,105 @@ +package org.mixer2.spring.boot; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mixer2.Mixer2Engine; +import org.mixer2.spring.webmvc.Mixer2XhtmlViewResolver; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.ViewResolver; + +import java.util.List; + +@Configuration +@ConditionalOnClass(Mixer2Engine.class) +@AutoConfigureAfter(WebMvcAutoConfiguration.class) +@EnableConfigurationProperties(Mixer2Properties.class) +public class Mixer2AutoConfiguration { + + private static Log log = LogFactory.getLog(Mixer2AutoConfiguration.class); + + @Bean + @ConditionalOnMissingBean + public Mixer2Engine mixer2Engine() { + return new Mixer2Engine(); + } + + @Configuration + @ConditionalOnClass(ViewResolver.class) + public static class Mixer2ViewConfiguration { + + private final Mixer2Engine mixer2Engine; + private final Mixer2Properties properties; + private final BeanFactory beanFactory; + + @Autowired + public Mixer2ViewConfiguration(Mixer2Engine mixer2Engine, Mixer2Properties properties, BeanFactory beanFactory) { + this.mixer2Engine = mixer2Engine; + this.properties = properties; + this.beanFactory = beanFactory; + } + + @Bean + @ConditionalOnMissingBean + public Mixer2XhtmlViewResolver mixer2XhtmlViewResolver() { + Mixer2XhtmlViewResolver resolver = new Mixer2XhtmlViewResolver(); + resolver.setOrder(properties.getOrder()); + resolver.setMixer2Engine(this.mixer2Engine); + resolver.setPrefix(properties.getPrefix()); + resolver.setSuffix(properties.getSuffix()); + if (properties.getViewBasePackage() != null) { + resolver.setBasePackage(properties.getViewBasePackage()); + } else if (AutoConfigurationPackages.has(beanFactory)) { + List packages = AutoConfigurationPackages.get(beanFactory); + if (packages.size() == 1) { + String viewBasePackage = packages.get(0) + ".web.view"; + resolver.setBasePackage(viewBasePackage); + if (log.isDebugEnabled()) { + log.debug("Apply '" + viewBasePackage + "' to the base package of view."); + } + } else { + if (log.isDebugEnabled()) { + log.debug("Skip auto-detecting base package of view because found multiple base packages. found base packages : " + packages); + } + } + } + if (properties.getViewClassNameSuffix() != null) { + resolver.setClassNameSuffix(properties.getViewClassNameSuffix()); + } + if (properties.getContentType() != null) { + resolver.setContentType(properties.getContentType()); + } + if (properties.getDocType() != null) { + resolver.setDocType(properties.getDocType()); + } + if (properties.getReturnNullIfTemplateFileNotFound() != null) { + resolver.setReturnNullIfTemplateFileNotFound(properties.getReturnNullIfTemplateFileNotFound()); + } + if (properties.getRaiseErrorIfViewClassNotFound() != null) { + resolver.setRaiseErrorIfViewClassNotFound(properties.getRaiseErrorIfViewClassNotFound()); + } + if (properties.getCache() != null) { + resolver.setCache(properties.getCache()); + } + if (resolver.isCache()) { + if (properties.getCacheLimit() != null) { + resolver.setCacheLimit(properties.getCacheLimit()); + } + if (properties.getCacheUnresolved() != null) { + resolver.setCacheUnresolved(properties.getCacheUnresolved()); + } + } + return resolver; + } + + } + +} diff --git a/src/main/java/org/mixer2/spring/boot/Mixer2Properties.java b/src/main/java/org/mixer2/spring/boot/Mixer2Properties.java new file mode 100644 index 0000000..66c6cb6 --- /dev/null +++ b/src/main/java/org/mixer2/spring/boot/Mixer2Properties.java @@ -0,0 +1,132 @@ +package org.mixer2.spring.boot; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; + +@ConfigurationProperties(prefix = "mixer2") +public class Mixer2Properties implements Serializable { + + private static final long serialVersionUID = -8282705230257476884L; + + private int order = 1; + + private String prefix = "classpath:/m2mockup/templates/"; + + private String suffix = ".html"; + + private String viewBasePackage; + + private String viewClassNameSuffix; + + private String docType; + + private String contentType; + + private Boolean returnNullIfTemplateFileNotFound; + + private Boolean raiseErrorIfViewClassNotFound; + + private Integer cacheLimit; + + private Boolean cacheUnresolved; + + private Boolean cache; + + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public String getViewBasePackage() { + return viewBasePackage; + } + + public void setViewBasePackage(String viewBasePackage) { + this.viewBasePackage = viewBasePackage; + } + + public String getViewClassNameSuffix() { + return viewClassNameSuffix; + } + + public void setViewClassNameSuffix(String viewClassNameSuffix) { + this.viewClassNameSuffix = viewClassNameSuffix; + } + + public String getDocType() { + return docType; + } + + public void setDocType(String docType) { + this.docType = docType; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Boolean getReturnNullIfTemplateFileNotFound() { + return returnNullIfTemplateFileNotFound; + } + + public void setReturnNullIfTemplateFileNotFound(Boolean returnNullIfTemplateFileNotFound) { + this.returnNullIfTemplateFileNotFound = returnNullIfTemplateFileNotFound; + } + + public Boolean getRaiseErrorIfViewClassNotFound() { + return raiseErrorIfViewClassNotFound; + } + + public void setRaiseErrorIfViewClassNotFound(Boolean raiseErrorIfViewClassNotFound) { + this.raiseErrorIfViewClassNotFound = raiseErrorIfViewClassNotFound; + } + + public Integer getCacheLimit() { + return cacheLimit; + } + + public void setCacheLimit(Integer cacheLimit) { + this.cacheLimit = cacheLimit; + } + + public Boolean getCacheUnresolved() { + return cacheUnresolved; + } + + public void setCacheUnresolved(Boolean cacheUnresolved) { + this.cacheUnresolved = cacheUnresolved; + } + + public Boolean getCache() { + return cache; + } + + public void setCache(Boolean cache) { + this.cache = cache; + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..1d20492 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.mixer2.spring.boot.Mixer2AutoConfiguration diff --git a/src/test/java/org/mixer2/spring/boot/Mixer2AutoConfigurationTest.java b/src/test/java/org/mixer2/spring/boot/Mixer2AutoConfigurationTest.java new file mode 100644 index 0000000..4d701c2 --- /dev/null +++ b/src/test/java/org/mixer2/spring/boot/Mixer2AutoConfigurationTest.java @@ -0,0 +1,237 @@ +package org.mixer2.spring.boot; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mixer2.Mixer2Engine; +import org.mixer2.jaxb.exception.Mixer2JAXBException; +import org.mixer2.jaxb.xhtml.Html; +import org.mixer2.spring.boot.app.view.CustomScreenView; +import org.mixer2.spring.boot.config.SubPackageAppConfig; +import org.mixer2.spring.boot.web.view.DefaultView; +import org.mixer2.spring.webmvc.AbstractMixer2XhtmlView; +import org.mixer2.spring.webmvc.Mixer2XhtmlViewResolver; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.View; + +import java.util.Locale; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertThat; + +public class Mixer2AutoConfigurationTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private AnnotationConfigApplicationContext context; + + @Before + public void init() { + this.context = new AnnotationConfigApplicationContext(); + } + + @After + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void testDefaultConfiguration() throws Exception { + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + assertThat(this.context.getBeanNamesForType(Mixer2Engine.class).length, is(1)); + + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + assertThat(viewResolver.getOrder(), is(1)); + assertThat(viewResolver.isCache(), is(true)); + assertThat(viewResolver.getCacheLimit(), is(1024)); + assertThat(viewResolver.isCacheUnresolved(), is(true)); + + DefaultView view = (DefaultView) viewResolver.resolveViewName("default", Locale.US); + MockHttpServletResponse response = new MockHttpServletResponse(); + view.render(null, new MockHttpServletRequest(), response); + assertThat(response.getContentAsString(), is("\n\ndefault\n")); + assertThat(response.getContentType(), is("text/html; charset=UTF-8")); + + } + + @Test + public void testCustomUsingProperties() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "mixer2.order:0", + "mixer2.prefix:classpath:/m2mockup/custom-templates/", + "mixer2.suffix:.htm", + "mixer2.view-base-package:org.mixer2.spring.boot.app.view", + "mixer2.view-class-name-suffix:ScreenView", + "mixer2.content-type:text/html; charset=UTF-16", + "mixer2.doc-type:"); + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + assertThat(this.context.getBeanNamesForType(Mixer2Engine.class).length, is(1)); + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + assertThat(viewResolver.getOrder(), is(0)); + + CustomScreenView view = (CustomScreenView) viewResolver.resolveViewName("custom", Locale.US); + MockHttpServletResponse response = new MockHttpServletResponse(); + view.render(null, new MockHttpServletRequest(), response); + assertThat(response.getContentAsString(), is("\n\ncustom default\n")); + assertThat(response.getContentType(), is("text/html; charset=UTF-16")); + } + + @Test + public void testCustomCacheIsFalseUsingProperties() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "mixer2.cache:false", + "mixer2.cache-limit:4000", + "mixer2.cache-unresolved:false"); + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + assertThat(this.context.getBeanNamesForType(Mixer2Engine.class).length, is(1)); + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + assertThat(viewResolver.isCache(), is(false)); + assertThat(viewResolver.getCacheLimit(), is(0)); + assertThat(viewResolver.isCacheUnresolved(), is(true)); + + } + + @Test + public void testCustomCacheParametersUsingProperties() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "mixer2.cache:true", + "mixer2.cache-limit:512", + "mixer2.cache-unresolved:false"); + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + assertThat(this.context.getBeanNamesForType(Mixer2Engine.class).length, is(1)); + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + assertThat(viewResolver.isCache(), is(true)); + assertThat(viewResolver.getCacheLimit(), is(512)); + assertThat(viewResolver.isCacheUnresolved(), is(false)); + } + + @Test + public void testNotFoundTemplateFileOnDefaultConfiguration() throws Exception { + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + View view = viewResolver.resolveViewName("foo", Locale.US); + assertThat(view, nullValue()); + } + + @Test + public void testNotFoundTemplateFileOnReturnNullIfTemplateFileNotFoundIsFalse() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "mixer2.return-null-if-template-file-not-found:false"); + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + AbstractMixer2XhtmlView view = (AbstractMixer2XhtmlView) viewResolver.resolveViewName("foo", Locale.US); + assertThat(view.getUrl(), is("classpath:/m2mockup/templates/foo.html")); + } + + + @Test + public void testNotFoundViewClassOnDefaultConfiguration() throws Exception { + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + View view = viewResolver.resolveViewName("bar", Locale.US); + MockHttpServletResponse response = new MockHttpServletResponse(); + view.render(null, new MockHttpServletRequest(), response); + assertThat(response.getContentAsString(), is("\n\nbar\n")); + assertThat(response.getContentType(), is("text/html; charset=UTF-8")); + + } + + @Test + public void testNotFoundViewClassOnRaiseErrorIfViewClassNotFoundIsTrue() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "mixer2.raise-error-if-view-class-not-found:true"); + this.context.register(AppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + expectedException.expect(ClassNotFoundException.class); + expectedException.expectMessage(is("org.mixer2.spring.boot.web.view.BarView")); + + Mixer2XhtmlViewResolver viewResolver = context.getBean(Mixer2XhtmlViewResolver.class); + viewResolver.resolveViewName("bar", Locale.US); + + } + + + @Test + public void testFoundMultipleBasePackage() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "mixer2.raise-error-if-view-class-not-found:true"); + this.context.register(AppConfig.class, SubPackageAppConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + expectedException.expect(ClassNotFoundException.class); + expectedException.expectMessage(is("BarView")); + + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + viewResolver.resolveViewName("bar", Locale.US); + + } + + @Test + public void testCustomConfiguration() throws Mixer2JAXBException { + this.context.register(CustomConfig.class, Mixer2AutoConfiguration.class); + this.context.refresh(); + + Mixer2Engine engine = this.context.getBean(Mixer2Engine.class); + try { + engine.checkAndLoadHtmlTemplate("foo"); + } catch (UnsupportedOperationException e) { + assertThat(e.getMessage(), is("error")); + } + + Mixer2XhtmlViewResolver viewResolver = this.context.getBean(Mixer2XhtmlViewResolver.class); + assertThat(viewResolver.isCache(), is(false)); + + } + + @SpringBootApplication + static class AppConfig { + } + + static class CustomConfig { + + @Bean + Mixer2Engine mixer2Engine() { + return new Mixer2Engine() { + @Override + public Html checkAndLoadHtmlTemplate(String str) throws Mixer2JAXBException { + throw new UnsupportedOperationException("error"); + } + }; + } + + @Bean + Mixer2XhtmlViewResolver mixer2XhtmlViewResolver() { + Mixer2XhtmlViewResolver viewResolver = new Mixer2XhtmlViewResolver(); + viewResolver.setCache(false); + return viewResolver; + } + + } + +} diff --git a/src/test/java/org/mixer2/spring/boot/app/view/CustomScreenView.java b/src/test/java/org/mixer2/spring/boot/app/view/CustomScreenView.java new file mode 100644 index 0000000..3ad7b07 --- /dev/null +++ b/src/test/java/org/mixer2/spring/boot/app/view/CustomScreenView.java @@ -0,0 +1,15 @@ +package org.mixer2.spring.boot.app.view; + +import org.mixer2.jaxb.xhtml.Html; +import org.mixer2.spring.webmvc.AbstractMixer2XhtmlView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public class CustomScreenView extends AbstractMixer2XhtmlView { + @Override + protected Html renderHtml(Html html, Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + return html; + } +} diff --git a/src/test/java/org/mixer2/spring/boot/config/SubPackageAppConfig.java b/src/test/java/org/mixer2/spring/boot/config/SubPackageAppConfig.java new file mode 100644 index 0000000..2b08d36 --- /dev/null +++ b/src/test/java/org/mixer2/spring/boot/config/SubPackageAppConfig.java @@ -0,0 +1,7 @@ +package org.mixer2.spring.boot.config; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; + +@EnableAutoConfiguration +public class SubPackageAppConfig { +} diff --git a/src/test/java/org/mixer2/spring/boot/web/view/DefaultView.java b/src/test/java/org/mixer2/spring/boot/web/view/DefaultView.java new file mode 100644 index 0000000..e8e11b4 --- /dev/null +++ b/src/test/java/org/mixer2/spring/boot/web/view/DefaultView.java @@ -0,0 +1,15 @@ +package org.mixer2.spring.boot.web.view; + +import org.mixer2.jaxb.xhtml.Html; +import org.mixer2.spring.webmvc.AbstractMixer2XhtmlView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public class DefaultView extends AbstractMixer2XhtmlView { + @Override + protected Html renderHtml(Html html, Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + return html; + } +} diff --git a/src/test/resources/m2mockup/custom-templates/custom.htm b/src/test/resources/m2mockup/custom-templates/custom.htm new file mode 100644 index 0000000..c2550ed --- /dev/null +++ b/src/test/resources/m2mockup/custom-templates/custom.htm @@ -0,0 +1 @@ +custom default \ No newline at end of file diff --git a/src/test/resources/m2mockup/templates/bar.html b/src/test/resources/m2mockup/templates/bar.html new file mode 100644 index 0000000..fdc31db --- /dev/null +++ b/src/test/resources/m2mockup/templates/bar.html @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/src/test/resources/m2mockup/templates/default.html b/src/test/resources/m2mockup/templates/default.html new file mode 100644 index 0000000..3500b1f --- /dev/null +++ b/src/test/resources/m2mockup/templates/default.html @@ -0,0 +1 @@ +default \ No newline at end of file