/*
* Copyright (c) 2001-2007, Inversoft Inc., All Rights Reserved
*
* Licensed 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
*
* 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.primeframework.mvc.control;
import javax.servlet.http.HttpServletRequest;
import java.io.Writer;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import org.primeframework.mvc.PrimeException;
import org.primeframework.mvc.action.ActionInvocation;
import org.primeframework.mvc.action.ActionInvocationStore;
import org.primeframework.mvc.config.MVCConfiguration;
import org.primeframework.mvc.control.annotation.ControlAttribute;
import org.primeframework.mvc.control.annotation.ControlAttributes;
import org.primeframework.mvc.control.form.JoinMethod;
import org.primeframework.mvc.freemarker.FreeMarkerService;
import org.primeframework.mvc.util.ErrorList;
import com.google.inject.Inject;
import freemarker.template.Configuration;
/**
* This class an abstract Control implementation that is useful for creating new controls that might need access to
* things such as the request, the action invocation and attributes.
*
* @author Brian Pontarelli
*/
public abstract class AbstractControl implements Control {
protected final Map<String, Object> attributes = new TreeMap<>();
protected final Map<String, String> dynamicAttributes = new TreeMap<>();
protected final Map<String, Object> parameters = new TreeMap<>();
protected ActionInvocationStore actionInvocationStore;
protected MVCConfiguration configuration;
protected Configuration freeMarkerConfig;
protected FreeMarkerService freeMarkerService;
protected Locale locale;
protected HttpServletRequest request;
protected Object root;
/**
* This implementation just calls the Body implementation to render the body.
*
* @param writer The writer to write the body to.
* @param body The body.
*/
public void renderBody(Writer writer, Body body) {
body.render(writer);
}
/**
* Implements the controls renderEnd method that is called directly by the JSP taglibs. This method is the main
* render
* point for the control and it uses the {@link FreeMarkerService} to render the control. Sub-classes need to
* implement a number of methods in order to setup the Map that is passed to FreeMarker as well as determine the name
* of the template
*
* @param writer The writer to output to.
*/
public void renderEnd(Writer writer) {
if (endTemplateName() != null) {
String templateName = configuration.resourceDirectory() + "/control-templates/" + endTemplateName();
freeMarkerService.render(writer, templateName, root);
}
}
/**
* Implements the controls renderStart method that is called directly by the JSP taglibs. This method is the main
* render point for the control and it uses the {@link FreeMarkerService} to render the control. Sub-classes need to
* implement a number of methods in order to setup the Map that is passed to FreeMarker as well as determine the name
* of the template.
*
* @param writer The writer to output to.
* @param attributes The attributes.
* @param dynamicAttributes The dynamic attributes from the tag. Dynamic attributes start with an underscore.
*/
public void renderStart(Writer writer, Map<String, Object> attributes, Map<String, String> dynamicAttributes) {
this.attributes.clear();
this.dynamicAttributes.clear();
this.parameters.clear();
this.attributes.putAll(attributes);
this.dynamicAttributes.putAll(dynamicAttributes);
verifyAttributes();
addAdditionalAttributes();
this.parameters.putAll(makeParameters());
this.root = makeRoot();
if (startTemplateName() != null) {
String templateName = configuration.resourceDirectory() + "/control-templates/" + startTemplateName();
freeMarkerService.render(writer, templateName, root);
}
}
@Inject
public void setServices(Locale locale, HttpServletRequest request, ActionInvocationStore actionInvocationStore,
FreeMarkerService freeMarkerService, MVCConfiguration configuration, Configuration freeMarkerConfig) {
this.locale = locale;
this.request = request;
this.freeMarkerService = freeMarkerService;
this.actionInvocationStore = actionInvocationStore;
this.configuration = configuration;
this.freeMarkerConfig = freeMarkerConfig;
}
/**
* Sub-classes can implement this method to add additional attributes. This is primarily used by control tags to
* determine values, checked states, selected options, etc.
*/
protected void addAdditionalAttributes() {
}
/**
* @return The control name, which is usually the simple class name all lowercased.
*/
protected String controlName() {
return getClass().getSimpleName().toLowerCase();
}
/**
* @return The current action or null.
*/
protected Object currentAction() {
return currentInvocation().action;
}
/**
* @return The current action invocation.
*/
protected ActionInvocation currentInvocation() {
return actionInvocationStore.getCurrent();
}
/**
* @return The name of the FreeMarker template that this control renders when it ends.
*/
protected abstract String endTemplateName();
/**
* Creates the parameters Map that is the root node used by the FreeMarker template when rendering. This places these
* values in the root map:
* <p>
* <ul>
* <li>attributes - The attributes</li>
* <li>dynamic_attributes - The dynamic attributes</li>
* </ul>
*
* @return The Parameters Map.
*/
protected Map<String, Object> makeParameters() {
Map<String, Object> parameters = new HashMap<>();
parameters.put("attributes", attributes);
parameters.put("dynamicAttributes", dynamicAttributes);
parameters.put("join", new JoinMethod(freeMarkerConfig.getObjectWrapper()));
return parameters;
}
/**
* Converts the given parameters into a FreeMarker root node. This can be overridden by sub-classes to convert the
* Map
* or wrap it. This method simply returns the given Map.
*
* @return The root.
*/
protected Object makeRoot() {
return parameters;
}
/**
* @return The name of the FreeMarker template that this control renders when it starts.
*/
protected abstract String startTemplateName();
private String toTypeListString(Class<?>[] attributeTypes) {
StringBuilder build = new StringBuilder();
for (int i = 0; i < attributeTypes.length; i++) {
Class<?> attributeType = attributeTypes[i];
build.append(attributeType.toString());
if (i == attributeTypes.length - 2) {
build.append(", or ");
} else if (i > 0) {
build.append(", ");
}
}
return build.toString();
}
private void verifyAttributes(Map<String, Object> attributes, ControlAttribute[] controlAttributes, boolean required,
ErrorList errors) {
for (ControlAttribute controlAttribute : controlAttributes) {
Object value = attributes.get(controlAttribute.name());
if (value == null && required) {
errors.addError("The control [" + controlName() + "] is missing the required attribute [" +
controlAttribute.name() + "]");
} else if (value != null) {
Class<?>[] attributeTypes = controlAttribute.types();
boolean found = false;
for (Class<?> attributeType : attributeTypes) {
found = attributeType.isInstance(value);
if (found) {
break;
}
}
if (!found) {
errors.addError("The control [" + controlName() + "] has an invalid attribute [" + controlAttribute.name() +
"] of type [" + value.getClass() + "]. It must be an instance of [" + toTypeListString(attributeTypes) + "]");
}
}
}
}
/**
* Verifies that all the attributes are correctly defined for the control.
*/
private void verifyAttributes() {
ErrorList errors = new ErrorList();
Class<?> type = getClass();
ControlAttributes ca = type.getAnnotation(ControlAttributes.class);
if (ca != null) {
ControlAttribute[] requiredAttributes = ca.required();
verifyAttributes(attributes, requiredAttributes, true, errors);
ControlAttribute[] optionalAttributes = ca.optional();
verifyAttributes(attributes, optionalAttributes, false, errors);
}
if (!errors.isEmpty()) {
throw new PrimeException(errors.toString());
}
}
}