package org.springframework.roo.addon.web.mvc.exceptions.addon; import static java.lang.reflect.Modifier.PUBLIC; import org.apache.commons.lang3.Validate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.osgi.framework.BundleContext; import org.osgi.service.component.ComponentContext; import org.springframework.roo.addon.web.mvc.controller.annotations.RooController; import org.springframework.roo.addon.web.mvc.exceptions.annotations.RooExceptionHandler; import org.springframework.roo.addon.web.mvc.exceptions.annotations.RooExceptionHandlers; import org.springframework.roo.addon.web.mvc.i18n.components.I18n; import org.springframework.roo.addon.web.mvc.i18n.components.I18nSupport; import org.springframework.roo.classpath.PhysicalTypeCategory; import org.springframework.roo.classpath.PhysicalTypeIdentifier; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.TypeManagementService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder; import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder; import org.springframework.roo.classpath.details.annotations.ArrayAttributeValue; import org.springframework.roo.classpath.details.annotations.ClassAttributeValue; import org.springframework.roo.classpath.details.annotations.NestedAnnotationAttributeValue; import org.springframework.roo.classpath.details.annotations.StringAttributeValue; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.RooJavaType; import org.springframework.roo.model.SpringJavaType; import org.springframework.roo.process.manager.FileManager; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.Path; import org.springframework.roo.project.PathResolver; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.propfiles.manager.PropFilesManagerService; import org.springframework.roo.support.ant.AntPathMatcher; import org.springframework.roo.support.logging.HandlerUtils; import org.springframework.roo.support.osgi.ServiceInstaceManager; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Logger; /** * Implementation of {@link ExceptionsOperations} interface. * * @author Fran Cardoso * @since 2.0 */ @Component @Service public class ExceptionsOperationsImpl implements ExceptionsOperations { private static final Logger LOGGER = HandlerUtils.getLogger(ExceptionsOperationsImpl.class); private static final String ERROR_VIEW = "errorView"; private static final String EXCEPTION = "exception"; private static final String LABEL_PREFIX = "label_"; private static final String LABEL_MESSAGE = "TODO Auto-generated %s message"; private static final String VALUE = "value"; @Reference TypeLocationService typeLocationService; // ------------ OSGi component attributes ---------------- private BundleContext context; private ServiceInstaceManager serviceInstaceManager = new ServiceInstaceManager(); protected void activate(final ComponentContext context) { this.context = context.getBundleContext(); serviceInstaceManager.activate(this.context); } /** * {@inheritDoc} */ @Override public void addExceptionHandler(JavaType exception, JavaType controller, JavaType adviceClass, String errorView) { // Check if exception class exists if (typeLocationService.getTypeDetails(exception) == null) { LOGGER.warning(String.format("Can't found class: %s", exception.getFullyQualifiedTypeName())); return; } // Check that both parameters 'controller' and 'class' are not defined if (controller != null && adviceClass != null) { LOGGER.warning("Only one of \"controller\" or \"class\" parameters must be defined"); return; } // Error view or exception response status is required if (errorView == null) { if (!isExceptionAnnotatedWithResponseStatus(exception)) { LOGGER.warning("Exception must be annotated with @ResponseStatus or an error view " + "must be provided"); return; } } if (controller == null && adviceClass != null) { // Create or update advice class if its correctly annotated if (createControllerAdviceIfRequired(adviceClass)) { addHandlersAnnotations(exception, adviceClass, errorView); addExceptionLabel(exception, adviceClass.getModule()); } } else if (controller != null) { if (typeLocationService.getTypeDetails(controller) == null) { LOGGER.warning(String.format("Can't found class: %s", controller.getFullyQualifiedTypeName())); return; } // Update controller class if its correctly annotated if (isRooController(controller)) { addHandlersAnnotations(exception, controller, errorView); addExceptionLabel(exception, controller.getModule()); } else { LOGGER.warning("Controller class must be annotated with @RooController"); return; } } else { LOGGER.warning("Target class is required"); return; } } /** * Writes a label with a default exception message on messages.properties files * * @param exception * @param moduleName */ private void addExceptionLabel(JavaType exception, String moduleName) { if (getProjectOperations().isMultimoduleProject()) { Validate.notBlank(moduleName, "Module name is required"); } final LogicalPath resourcesPath = LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, moduleName); final String targetDirectory = getPathResolver().getIdentifier(resourcesPath, ""); final String exceptionName = exception.getSimpleTypeName(); final String labelKey = LABEL_PREFIX.concat(exceptionName.toLowerCase()); Set<I18n> supportedLanguages = getI18nSupport().getSupportedLanguages(); for (I18n i18n : supportedLanguages) { String messageBundle = String.format("messages_%s.properties", i18n.getLocale().getLanguage()); String bundlePath = String.format("%s%s%s", targetDirectory, AntPathMatcher.DEFAULT_PATH_SEPARATOR, messageBundle); if (getFileManager().exists(bundlePath)) { getPropFilesManager().addPropertyIfNotExists(resourcesPath, messageBundle, labelKey, String.format(LABEL_MESSAGE, exceptionName), true); } } // Always update english message bundles getPropFilesManager().addPropertyIfNotExists(resourcesPath, "messages.properties", labelKey, String.format(LABEL_MESSAGE, exceptionName), true); } /** * Creates a new class annotated with @ControllerAdvice if not exists. Returns true if success * or class already exists. * * Returns false if class already exists but it's not annotated with @ControllerAdvice. * * @param controllerAdviceClass */ private boolean createControllerAdviceIfRequired(JavaType controllerAdviceClass) { // Checks if new service interface already exists. final String controllerAdviceClassIdentifier = getPathResolver().getCanonicalPath(controllerAdviceClass.getModule(), Path.SRC_MAIN_JAVA, controllerAdviceClass); if (!getFileManager().exists(controllerAdviceClassIdentifier)) { // Creating class builder final String mid = PhysicalTypeIdentifier.createIdentifier(controllerAdviceClass, getPathResolver().getPath(controllerAdviceClassIdentifier)); final ClassOrInterfaceTypeDetailsBuilder typeBuilder = new ClassOrInterfaceTypeDetailsBuilder(mid, PUBLIC, controllerAdviceClass, PhysicalTypeCategory.CLASS); typeBuilder.addAnnotation(new AnnotationMetadataBuilder(SpringJavaType.CONTROLLER_ADVICE) .build()); // Write new class disk getTypeManagementService().createOrUpdateTypeOnDisk(typeBuilder.build()); } else { // Check if class is annotated with @ControllerAdvice ClassOrInterfaceTypeDetails typeDetails = typeLocationService.getTypeDetails(controllerAdviceClass); AnnotationMetadata annotation = typeDetails.getAnnotation(SpringJavaType.CONTROLLER_ADVICE); if (annotation == null) { LOGGER.warning("Class must be annotated with @ControllerAdvice"); return false; } } return true; } /** * Generates {@link RooExceptionHandlers} and {@link RooExceptionHandler} annotations * and adds or updates it on specified class. * * @param exception * @param targetClass * @param errorView */ private void addHandlersAnnotations(JavaType exception, JavaType targetClass, String errorView) { Validate.notNull(targetClass, "Target class is required to add @RooExceptionHandlers annotation"); // Create @RooExceptionHandler Annotation final AnnotationMetadataBuilder exceptionHandlerAnnotationBuilder = new AnnotationMetadataBuilder(RooJavaType.ROO_EXCEPTION_HANDLER); final List<AnnotationAttributeValue<?>> exceptionHandlerAnnotationAttributes = new ArrayList<AnnotationAttributeValue<?>>(); exceptionHandlerAnnotationAttributes.add(new ClassAttributeValue(new JavaSymbolName(EXCEPTION), exception)); if (errorView != null) { exceptionHandlerAnnotationAttributes.add(new StringAttributeValue(new JavaSymbolName( ERROR_VIEW), errorView)); } exceptionHandlerAnnotationBuilder.setAttributes(exceptionHandlerAnnotationAttributes); // Check if container annotation already exists ClassOrInterfaceTypeDetails typeDetails = typeLocationService.getTypeDetails(targetClass); ClassOrInterfaceTypeDetailsBuilder typeDetailsBuilder = new ClassOrInterfaceTypeDetailsBuilder(typeDetails); AnnotationMetadata exceptionHandlersAnnotation = typeDetails.getAnnotation(RooJavaType.ROO_EXCEPTION_HANDLERS); AnnotationMetadataBuilder exceptionHandlersAnnotationBuilder = null; if (exceptionHandlersAnnotation != null) { exceptionHandlersAnnotationBuilder = new AnnotationMetadataBuilder(exceptionHandlersAnnotation); } else { exceptionHandlersAnnotationBuilder = new AnnotationMetadataBuilder(RooJavaType.ROO_EXCEPTION_HANDLERS); } Validate.notNull(exceptionHandlersAnnotationBuilder); // Add @RooExceptionHandler annotation into @RooExceptionHandlers final List<NestedAnnotationAttributeValue> exceptionHandlersArrayValues = new ArrayList<NestedAnnotationAttributeValue>(); exceptionHandlersArrayValues.add(new NestedAnnotationAttributeValue(new JavaSymbolName(VALUE), exceptionHandlerAnnotationBuilder.build())); final List<AnnotationAttributeValue<?>> attributeValues = new ArrayList<AnnotationAttributeValue<?>>(); attributeValues.add(new ArrayAttributeValue<NestedAnnotationAttributeValue>(new JavaSymbolName( VALUE), exceptionHandlersArrayValues)); if (exceptionHandlersAnnotation == null) { // Add new @RooExceptionHandlers annotation with given values exceptionHandlersAnnotationBuilder.setAttributes(attributeValues); typeDetailsBuilder.addAnnotation(exceptionHandlersAnnotationBuilder.build()); } else { // Get current annotation values from @RooExceptionHandlers annotation AnnotationAttributeValue<?> currentHandlers = exceptionHandlersAnnotation.getAttribute(VALUE); if (currentHandlers != null) { List<?> values = (List<?>) currentHandlers.getValue(); Iterator<?> it = values.iterator(); while (it.hasNext()) { NestedAnnotationAttributeValue handler = (NestedAnnotationAttributeValue) it.next(); if (handler.getValue() != null) { // Check if there is a @RooExceptionHandlers with same 'exception' value if (exceptionHandlerAnnotationBuilder.build().getAttribute(EXCEPTION).getValue() .equals(handler.getValue().getAttribute(EXCEPTION).getValue())) { LOGGER.warning(String.format( "There is already a handler for exception %s in class %s", exception.getSimpleTypeName(), targetClass.getSimpleTypeName())); return; } exceptionHandlersArrayValues.add(handler); } } } // Add found values attributeValues.add(new ArrayAttributeValue<NestedAnnotationAttributeValue>( new JavaSymbolName(VALUE), exceptionHandlersArrayValues)); exceptionHandlersAnnotationBuilder.setAttributes(attributeValues); // Update annotation typeDetailsBuilder.updateTypeAnnotation(exceptionHandlersAnnotationBuilder.build()); } // Write to disk getTypeManagementService().createOrUpdateTypeOnDisk(typeDetailsBuilder.build()); } /** * Returns true if a class is annotated with {@link RooController} * * @param klass * @return */ private boolean isRooController(JavaType klass) { ClassOrInterfaceTypeDetails typeDetails = typeLocationService.getTypeDetails(klass); Validate.notNull(typeDetails, String.format("Can't found class: %s", klass.getFullyQualifiedTypeName())); AnnotationMetadata annotation = typeDetails.getAnnotation(RooJavaType.ROO_CONTROLLER); if (annotation == null) { return false; } return true; } private boolean isExceptionAnnotatedWithResponseStatus(JavaType exception) { ClassOrInterfaceTypeDetails exceptionCid = typeLocationService.getTypeDetails(exception); Validate.notNull(exceptionCid, String.format("Can't found class: %s", exception.getFullyQualifiedTypeName())); AnnotationMetadata annotation = exceptionCid.getAnnotation(SpringJavaType.RESPONSE_STATUS); if (annotation == null) { return false; } return true; } // Get OSGi services private ProjectOperations getProjectOperations() { return serviceInstaceManager.getServiceInstance(this, ProjectOperations.class); } private PropFilesManagerService getPropFilesManager() { return serviceInstaceManager.getServiceInstance(this, PropFilesManagerService.class); } private I18nSupport getI18nSupport() { return serviceInstaceManager.getServiceInstance(this, I18nSupport.class); } private FileManager getFileManager() { return serviceInstaceManager.getServiceInstance(this, FileManager.class); } private PathResolver getPathResolver() { return serviceInstaceManager.getServiceInstance(this, PathResolver.class); } private TypeManagementService getTypeManagementService() { return serviceInstaceManager.getServiceInstance(this, TypeManagementService.class); } }