From f69f2e02bca4198d0bc4a39670efb0eca7621428 Mon Sep 17 00:00:00 2001 From: Drew Wills Date: Tue, 14 Feb 2017 14:27:08 -0700 Subject: [PATCH] UP-4792: Refector to create new service 'ModelAttributeService'; add unit tests for ModelAttributeService --- build.gradle | 3 +- gradle.properties | 2 + uPortal-soffit-renderer/build.gradle | 3 + .../renderer/ModelAttributeService.java | 184 ++++++++++++++++ .../renderer/SoffitRendererConfiguration.java | 15 +- .../renderer/SoffitRendererController.java | 123 +---------- .../renderer/ModelAttributeServiceTest.java | 200 ++++++++++++++++++ 7 files changed, 406 insertions(+), 124 deletions(-) create mode 100644 uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/ModelAttributeService.java create mode 100644 uPortal-soffit-renderer/src/test/java/org/apereo/portal/soffit/renderer/ModelAttributeServiceTest.java diff --git a/build.gradle b/build.gradle index ce5c80a7e71..458bbcf85ad 100644 --- a/build.gradle +++ b/build.gradle @@ -34,9 +34,10 @@ buildScan { } subprojects { + apply plugin: 'checkstyle' + apply plugin: 'idea' apply plugin: 'java' apply plugin: 'maven' - apply plugin: 'checkstyle' repositories { mavenLocal() diff --git a/gradle.properties b/gradle.properties index 4fd682d280f..8b85863be26 100644 --- a/gradle.properties +++ b/gradle.properties @@ -51,5 +51,7 @@ hibernateVersion=4.2.19.Final httpclientVersion=4.5.2 jasyptVersion=1.9.2 jjwtVersion=0.6.0 +junitVersion=4.12 +mockitoVersion=2.7.6 slf4jVersion=1.7.21 springVersion=3.2.9.RELEASE diff --git a/uPortal-soffit-renderer/build.gradle b/uPortal-soffit-renderer/build.gradle index 9099e2bf48f..8f7c96733a2 100644 --- a/uPortal-soffit-renderer/build.gradle +++ b/uPortal-soffit-renderer/build.gradle @@ -5,5 +5,8 @@ dependencies { compile "org.springframework:spring-webmvc:${springVersion}" + testCompile "junit:junit:${junitVersion}" + testCompile "org.mockito:mockito-core:${mockitoVersion}" + provided "${servletApiDependency}" } diff --git a/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/ModelAttributeService.java b/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/ModelAttributeService.java new file mode 100644 index 00000000000..828bd5fd6a5 --- /dev/null +++ b/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/ModelAttributeService.java @@ -0,0 +1,184 @@ +/** + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apereo.portal.soffit.renderer; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apereo.portal.soffit.model.v1_0.Bearer; +import org.apereo.portal.soffit.model.v1_0.Definition; +import org.apereo.portal.soffit.model.v1_0.PortalRequest; +import org.apereo.portal.soffit.model.v1_0.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +/** + * Responsible for marshalling data required for rendering based on the + * {@link SoffitModelAttribute} annotation. + * + * @since 5.0 + * @author drewwills + */ +public class ModelAttributeService { + + @Autowired + private ApplicationContext applicationContext; + + private Map modelAttributes; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @PostConstruct + public void init() { + /* + * Gather classes & methods that reference @SoffitMoldelAttribute + */ + final Map map = new HashMap<>(); + final String[] beanNames = applicationContext.getBeanDefinitionNames(); + for (String name : beanNames) { + final Object bean = applicationContext.getBean(name); + final Class clazz = AopUtils.isAopProxy(bean) + ? AopUtils.getTargetClass(bean) + : bean.getClass(); + if (clazz.isAnnotationPresent(SoffitModelAttribute.class)) { + // The bean itself is the model attribute + map.put(clazz, bean); + } else { + // Check the bean for annotated methods... + for (Method m : clazz.getMethods()) { + if (m.isAnnotationPresent(SoffitModelAttribute.class)) { + map.put(m, bean); + } + } + } + } + logger.debug("Found {} beans and/or methods referencing @SoffitModelAttribute", map.size()); + modelAttributes = Collections.unmodifiableMap(map); + } + + /* package-private */ Map gatherModelAttributes(String viewName, HttpServletRequest req, + HttpServletResponse res, PortalRequest portalRequest, Bearer bearer, Preferences preferences, + Definition definition) { + + final Map rslt = new HashMap<>(); + + logger.debug("Processing model attributes for viewName='{}'", viewName); + + for (Map.Entry y : modelAttributes.entrySet()) { + final AnnotatedElement annotatedElement = y.getKey(); + final Object bean = y.getValue(); + final SoffitModelAttribute sma = annotatedElement.getAnnotation(SoffitModelAttribute.class); + if (attributeAppliesToView(sma, viewName)) { + logger.debug("The following SoffitModelAttribute applies to viewName='{}': {}", viewName, sma); + final String modelAttributeName = sma.value(); + // Are we looking at a class or a method? + if (annotatedElement instanceof Class) { + // The bean itself is the model attribute + rslt.put(modelAttributeName, bean); + } else if (annotatedElement instanceof Method) { + final Method m = (Method) annotatedElement; + final Object modelAttribute = getModelAttributeFromMethod(bean, m, req, res, + portalRequest, bearer, preferences, definition); + rslt.put(modelAttributeName, modelAttribute); + } else { + final String msg = "Unsupported AnnotatedElement type: " + AnnotatedElement.class.getName(); + throw new UnsupportedOperationException(msg); + } + } + } + + logger.debug("Calculated the following model attributes for viewName='{}': {}", viewName, rslt); + + return rslt; + + } + + protected Object getModelAttributeFromMethod(Object bean, Method method, HttpServletRequest req, HttpServletResponse res, + PortalRequest portalRequest, Bearer bearer, Preferences preferences, Definition definition) { + + // This Method must NOT have a void return type... + if (method.getReturnType().equals(Void.TYPE)) { + final String msg = "Methods annotated with SoffitModelAttribute must not specify a void return type; " + method.getName(); + throw new IllegalStateException(msg); + } + final Object[] parameters = prepareMethodParameters(method, req, res, + portalRequest, bearer, preferences, definition); + try { + final Object rslt = method.invoke(bean, parameters); + return rslt; + } catch (IllegalAccessException | InvocationTargetException e) { + final String msg = "Failed to generate a model attribute by invoking '" + method.getName() + + "' on the following bean: " + bean.toString(); + throw new RuntimeException(msg); + } + + } + + protected Object[] prepareMethodParameters(Method method, HttpServletRequest req, HttpServletResponse res, + PortalRequest portalRequest, Bearer bearer, Preferences preferences, Definition definition) { + + // Examine the parameters this Method declares and try to match them. + final Class[] parameterTypes = method.getParameterTypes(); + final Object[] rslt = new Object[parameterTypes.length]; + for (int i=0; i < rslt.length; i++) { + final Class pType = parameterTypes[i]; + // At present, these are the parameter types we support... + if (HttpServletRequest.class.equals(pType)) { + rslt[i] = req; + } else if (HttpServletResponse.class.equals(pType)) { + rslt[i] = res; + } else if (PortalRequest.class.equals(pType)) { + rslt[i] = portalRequest; + } else if (Bearer.class.equals(pType)) { + rslt[i] = bearer; + } else if (Preferences.class.equals(pType)) { + rslt[i] = preferences; + } else if (Definition.class.equals(pType)) { + rslt[i] = definition; + } else { + final String msg = "Unsupported parameter type for SoffitModelAttribute method: " + pType; + throw new UnsupportedOperationException(msg); + } + } + + return rslt; + + } + + protected boolean attributeAppliesToView(SoffitModelAttribute attributeAnnotation, String viewName) { + final Pattern pattern = Pattern.compile(attributeAnnotation.viewRegex()); + final Matcher matcher = pattern.matcher(viewName); + return matcher.matches(); + } + +} diff --git a/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererConfiguration.java b/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererConfiguration.java index 129a5a39a93..6f37bfa6306 100644 --- a/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererConfiguration.java +++ b/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererConfiguration.java @@ -37,11 +37,6 @@ @Configuration public class SoffitRendererConfiguration { - @Bean - public SoffitRendererController soffitRendererController() { - return new SoffitRendererController(); - } - @Bean public BearerService bearerService() { return new BearerService(); @@ -62,4 +57,14 @@ public DefinitionService definitionService() { return new DefinitionService(); } + @Bean + public ModelAttributeService modelAttributeService() { + return new ModelAttributeService(); + } + + @Bean + public SoffitRendererController soffitRendererController() { + return new SoffitRendererController(); + } + } diff --git a/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererController.java b/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererController.java index 885f4155d87..d028fe075e3 100644 --- a/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererController.java +++ b/uPortal-soffit-renderer/src/main/java/org/apereo/portal/soffit/renderer/SoffitRendererController.java @@ -19,18 +19,11 @@ package org.apereo.portal.soffit.renderer; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -47,10 +40,8 @@ import org.apereo.portal.soffit.service.PreferencesService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; @@ -106,9 +97,6 @@ public class SoffitRendererController { private static final String DEFAULT_MODE = "view"; private static final String DEFAULT_WINDOW_STATE = "normal"; - @Autowired - private ApplicationContext applicationContext; - @Autowired private Environment environment; @@ -124,42 +112,15 @@ public class SoffitRendererController { @Autowired private DefinitionService definitionService; + @Autowired + private ModelAttributeService modelAttributeService; + @Value("${soffit.renderer.viewsLocation:/WEB-INF/soffit/}") private String viewsLocation; private final Map availableViews = new HashMap<>(); - private Map modelAttributes; - protected final Logger logger = LoggerFactory.getLogger(getClass()); - @PostConstruct - public void init() { - /* - * Gather classes & methods that reference @SoffitMoldelAttribute - */ - final Map map = new HashMap<>(); - final String[] beanNames = applicationContext.getBeanDefinitionNames(); - for (String name : beanNames) { - final Object bean = applicationContext.getBean(name); - final Class clazz = AopUtils.isAopProxy(bean) - ? AopUtils.getTargetClass(bean) - : bean.getClass(); - if (clazz.isAnnotationPresent(SoffitModelAttribute.class)) { - // The bean itself is the model attribute - map.put(clazz, bean); - } else { - // Check the bean for annotated methods... - for (Method m : clazz.getMethods()) { - if (m.isAnnotationPresent(SoffitModelAttribute.class)) { - map.put(m, bean); - } - } - } - } - logger.debug("Found {} beans and/or methods referencing @SoffitModelAttribute", map.size()); - modelAttributes = Collections.unmodifiableMap(map); - } - @RequestMapping(value="/{module}", method=RequestMethod.GET) public ModelAndView render(final HttpServletRequest req, final HttpServletResponse res, final @PathVariable String module) { @@ -174,7 +135,7 @@ public ModelAndView render(final HttpServletRequest req, final HttpServletRespon // Select a view final String viewName = selectView(req, module, portalRequest); - final Map model = gatherModelAttributes(viewName, req, res, portalRequest, bearer, preferences, definition); + final Map model = modelAttributeService.gatherModelAttributes(viewName, req, res, portalRequest, bearer, preferences, definition); model.put(PORTAL_REQUEST_MODEL_NAME, portalRequest); model.put(BEARER_MODEL_NAME, bearer); model.put(PREFERENCES_MODEL_NAME, preferences); @@ -183,7 +144,7 @@ public ModelAndView render(final HttpServletRequest req, final HttpServletRespon // Set up cache headers appropriately configureCacheHeaders(res, module); - return new ModelAndView(viewName.toString(), model); + return new ModelAndView(viewName, model); } @@ -289,80 +250,6 @@ private String getCompletePathforParts(final String... parts) { } - private Map gatherModelAttributes(String viewName, HttpServletRequest req, HttpServletResponse res, - PortalRequest portalRequest, Bearer bearer, Preferences preferences, Definition definition) { - - final Map rslt = new HashMap<>(); - - logger.debug("Processing model attributes for viewName='{}'", viewName); - - for (Map.Entry y : modelAttributes.entrySet()) { - final AnnotatedElement annotatedElement = y.getKey(); - final Object bean = y.getValue(); - final SoffitModelAttribute sma = annotatedElement.getAnnotation(SoffitModelAttribute.class); - if (attributeAppliesToView(sma, viewName)) { - logger.debug("The following SoffitModelAttribute applies to viewName='{}': {}", viewName, sma); - final String modelAttributeName = sma.value(); - // Are we looking at a class or a method? - if (annotatedElement instanceof Class) { - // The bean itself is the model attribute - rslt.put(modelAttributeName, bean); - } else if (annotatedElement instanceof Method) { - Method m = (Method) annotatedElement; - // This Method must NOT have a void return type... - if (m.getReturnType().equals(Void.TYPE)) { - final String msg = "Methods annotated with SoffitModelAttribute must not specify a void return type; " + m.getName(); - throw new IllegalStateException(msg); - } - // Examine the parameters this Method declares and try to match them. - final Class[] parameterTypes = m.getParameterTypes(); - final Object[] parameters = new Object[parameterTypes.length]; - for (int i=0; i < parameters.length; i++) { - final Class pType = parameterTypes[i]; - // At present, these are the parameter types we support... - if (HttpServletRequest.class.equals(pType)) { - parameters[i] = req; - } else if (HttpServletResponse.class.equals(pType)) { - parameters[i] = res; - } else if (PortalRequest.class.equals(pType)) { - parameters[i] = portalRequest; - } else if (Bearer.class.equals(pType)) { - parameters[i] = bearer; - } else if (Preferences.class.equals(pType)) { - parameters[i] = preferences; - } else if (Definition.class.equals(pType)) { - parameters[i] = definition; - } else { - final String msg = "Unsupported parameter type for SoffitModelAttribute method: " + pType; - throw new UnsupportedOperationException(msg); - } - } - try { - final Object value = m.invoke(bean, parameters); - rslt.put(modelAttributeName, value); - } catch (IllegalAccessException | InvocationTargetException e) { - final String msg = "Failed to evaluate the specified model attribute: " + sma.value(); - throw new RuntimeException(msg); - } - } else { - final String msg = "Unsuppored AnnotatedElement type: " + AnnotatedElement.class.getName(); - throw new UnsupportedOperationException(msg); - } - } - } - - logger.debug("Calculated the following model attributes for viewName='{}': {}", viewName, rslt); - - return rslt; - - } - - private boolean attributeAppliesToView(SoffitModelAttribute attributeAnnotation, String viewName) { - final Pattern pattern = Pattern.compile(attributeAnnotation.viewRegex()); - final Matcher matcher = pattern.matcher(viewName); - return matcher.matches(); - } - private void configureCacheHeaders(final HttpServletResponse res, final String module) { final String cacheScopeProperty = String.format(CACHE_SCOPE_PROPERTY_FORMAT, module); diff --git a/uPortal-soffit-renderer/src/test/java/org/apereo/portal/soffit/renderer/ModelAttributeServiceTest.java b/uPortal-soffit-renderer/src/test/java/org/apereo/portal/soffit/renderer/ModelAttributeServiceTest.java new file mode 100644 index 00000000000..753e6a47085 --- /dev/null +++ b/uPortal-soffit-renderer/src/test/java/org/apereo/portal/soffit/renderer/ModelAttributeServiceTest.java @@ -0,0 +1,200 @@ +/** + * Licensed to Apereo under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Apereo licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apereo.portal.soffit.renderer; + +import static org.junit.Assert.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apereo.portal.soffit.model.v1_0.Bearer; +import org.apereo.portal.soffit.model.v1_0.Definition; +import org.apereo.portal.soffit.model.v1_0.PortalRequest; +import org.apereo.portal.soffit.model.v1_0.Preferences; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * @author drewwills + */ +public class ModelAttributeServiceTest extends ModelAttributeService { + + private final Object returnValue = new Object(); + + @Test + public void testGetModelAttributeFromMethod() { + + final ModelAttributeService modelAttributeService = new ModelAttributeService(); + + final Class[] parameterClasses = new Class[] { HttpServletRequest.class, PortalRequest.class, Bearer.class }; + final Method method; + try { + method = getClass().getMethod("soffitModelAttributeMethod", parameterClasses); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + // Object Model + final HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + final PortalRequest portalRequest = Mockito.mock(PortalRequest.class); + final Bearer bearer = Mockito.mock(Bearer.class); + final Preferences preferences = Mockito.mock(Preferences.class); + final Definition definition = Mockito.mock(Definition.class); + + final Object modelAttribute = modelAttributeService.getModelAttributeFromMethod(this, + method, req, res, portalRequest, bearer, preferences, definition); + + assertEquals("Incorrect modelAttribute", modelAttribute, returnValue); + + final Method brokenMethod; + try { + brokenMethod = getClass().getMethod("brokenSoffitModelAttributeMethod", parameterClasses); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + try { + modelAttributeService.getModelAttributeFromMethod(this, + brokenMethod, req, res, portalRequest, bearer, preferences, definition); + fail("Expected IllegalStateException for void-declaring method"); + } catch (IllegalStateException e) { + // Expected; fall through... + } + + } + + @Test + public void testPrepareMethodParameters() { + + final ModelAttributeService modelAttributeService = new ModelAttributeService(); + + final Class[] parameterClasses = new Class[] { HttpServletRequest.class, PortalRequest.class, Bearer.class }; + final Method method; + try { + method = getClass().getMethod("soffitModelAttributeMethod", parameterClasses); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + + // Object Model + final HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + final HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + final PortalRequest portalRequest = Mockito.mock(PortalRequest.class); + final Bearer bearer = Mockito.mock(Bearer.class); + final Preferences preferences = Mockito.mock(Preferences.class); + final Definition definition = Mockito.mock(Definition.class); + + final Object[] parameters = modelAttributeService.prepareMethodParameters(method, req, res, + portalRequest, bearer, preferences, definition); + + assertEquals("parameterClasses and parameters arrays must be the same length", + parameterClasses.length, parameters.length); + + for (int i=0; i < parameters.length; i++) { + assertTrue("Mismatched parameter type", parameterClasses[i].isInstance(parameters[i])); + } + + } + + @Test + public void testAttributeAppliesToView() { + + final ModelAttributeService modelAttributeService = new ModelAttributeService(); + + final SoffitModelAttribute unspecific = new BaseSoffitModelAttribute() { + @Override + public String value() { + return "unspecific"; + } + + @Override + public String viewRegex() { + return ".*"; + } + }; + + final SoffitModelAttribute specific = new BaseSoffitModelAttribute() { + @Override + public String value() { + return "specific"; + } + + @Override + public String viewRegex() { + return "specific"; + } + }; + + final String[] unspecificViewNames = new String[] {"apereo", "uPortal", "open source"}; + for (String name : unspecificViewNames) { + // Each of these should match 'unspecific' but not 'specific' + assertTrue("View name '" + name + "' should apply to model attribute: " + unspecific, + modelAttributeService.attributeAppliesToView(unspecific, name)); + assertFalse("View name '" + name + "' should NOT apply to model attribute: " + specific, + modelAttributeService.attributeAppliesToView(specific, name)); + } + + // But the name 'specific' should match... + final String specificName = "specific"; + assertTrue("View name '" + specificName + "' should apply to model attribute: " + specific, + modelAttributeService.attributeAppliesToView(unspecific, specificName)); + + } + + /** + * For use in some of the test cases via reflection. + */ + public Object soffitModelAttributeMethod(HttpServletRequest req, PortalRequest portalRequest, Bearer bearer) { + return returnValue; + } + + /** + * For use in some of the test cases via reflection. + */ + public void brokenSoffitModelAttributeMethod(HttpServletRequest req, PortalRequest portalRequest, Bearer bearer) { + // do nothing... + } + + private static abstract class BaseSoffitModelAttribute implements SoffitModelAttribute { + @Override + public boolean equals(Object obj) { + return false; + } + @Override + public int hashCode() { + return 0; + } + @Override + public String toString() { + return new ToStringBuilder(this) + .toString(); + } + @Override + public Class annotationType() { + return getClass(); + } + }; + +}