package org.jboss.seam.remoting.validation;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.enterprise.context.Conversation;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.MessageInterpolator;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.PropertyDescriptor;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jboss.solder.logging.Logger;
import org.jboss.seam.remoting.AnnotationsParser;
import org.jboss.seam.remoting.RequestHandler;
import org.jboss.seam.remoting.util.JsConverter;
import org.jboss.seam.remoting.util.Strings;
/**
* This class reads constraints metadata from all requested beans, translate them and send them back to client
*
* @author Amir Sadri
*/
public class ConstraintTranslator implements RequestHandler {
/////////TODO what if client wanted to use a customized ValidatorFactory
private static final ValidatorFactory Factory = Validation.buildDefaultValidatorFactory();
private static final Logger log = Logger.getLogger(ConstraintTranslator.class);
private static Annotation[] EMPTY_ANNOTATIONS = new Annotation[]{};
private static final byte[] VALIDATION_TAG_OPEN = "<validation>".getBytes();
private static final byte[] VALIDATION_TAG_CLOSE = "</validation>".getBytes();
private static final byte[] MESSAGES_TAG_OPEN = "<messages>".getBytes();
private static final byte[] MESSAGES_TAG_CLOSE = "</messages>".getBytes();
private static final byte[] BEAN_TAG_OPEN_START = "<b n=\"".getBytes();
private static final byte[] BEAN_TAG_OPEN_END = "\">".getBytes();
private static final byte[] BEAN_TAG_CLOSE = "</b>".getBytes();
private static final byte[] PROPERTY_TAG_OPEN_START = "<p n=\"".getBytes();
private static final byte[] PROPERTY_TAG_OPEN_END = "\">".getBytes();
private static final byte[] PROPERTY_TAG_CLOSE = "</p>".getBytes();
private static final byte[] CONSTRAINT_TAG_OPEN_START = "<c n=\"".getBytes();
private static final byte[] CONSTRAINT_TAG_MIDDLE = "\" parent=\"".getBytes();
private static final byte[] CONSTRAINT_TAG_OPEN_END = "\">".getBytes();
private static final byte[] CONSTRAINT_TAG_CLOSE = "</c>".getBytes();
private static final byte[] GROUP_TAG_OPEN = "<g id=\"".getBytes();
private static final byte[] GROUP_TAG_CLOSE = "\" />".getBytes();
private static final String GROUP_HIERARCHY_TAG_OPEN = "<gh id=\"";
private static final String GROUP_HIERARCHY_TAG_MIDDLE = "\" n=\"";
private static final String GROUP_HIERARCHY_TAG_OPEN_END = "\">";
private static final String GROUP_HIERARCHY_CLOSE = "</gh>";
private static final String GROUP_PARENT_TAG_OPEN = "<gp ";
private static final String GROUP_PARENT_TAG_MIDDLE_ID = " id=\"";
private static final String GROUP_PARENT_TAG_MIDDLE_NAME = " n=\"";
private static final String GROUP_PARENT_TAG_CLOSE = "\" />";
private static final byte[] MESSAGE_TAG_OPEN_START = "<m c=\"".getBytes();
private static final byte[] MESSAGE_TAG_OPEN_MIDDLE = "\" msg=\"".getBytes();
private static final byte[] MESSAGE_TAG_OPEN_END = "\" />".getBytes();
private static final byte[] PARAMETER_TAG_OPEN_START = "<pm n=\"".getBytes();
private static final byte[] PARAMETER_TAG_OPEN_MIDDLE = "\" v=\"".getBytes();
private static final byte[] PARAMETER_TAG_OPEN_END = "\" />".getBytes();
static ArrayList<String> DEFAULT_ATTRIBUTES = new ArrayList<String>();
static {
DEFAULT_ATTRIBUTES.add("message");
DEFAULT_ATTRIBUTES.add("groups");
DEFAULT_ATTRIBUTES.add("payload");
}
private HashMap<String, SpecialConsideration> specialConsiderations = new HashMap<String, SpecialConsideration>();
@Inject
BeanManager beanManager;
@Inject
Conversation conversation;
public ConstraintTranslator() {
specialConsiderations.put("Pattern", new RegexpConsideration());
//////TODO implement a mechanism which would allow developers to add their own SpecialConsiderations dynamically
}
/*
* handles validation requests by converting Constraint metadata to a client-side readable, XML format
*
* @see org.jboss.seam.remoting.RequestHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@SuppressWarnings("unchecked")
public void handle(HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType("text/xml");
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[256];
int read = request.getInputStream().read(buffer);
while (read != -1) {
out.write(buffer, 0, read);
read = request.getInputStream().read(buffer);
}
String requestData = new String(out.toByteArray());
log.debug("[ConstraintHandler] Processing remote request: " + requestData);
// Parse the incoming request as XML
SAXReader xmlReader = new SAXReader();
Document doc = xmlReader.read(new StringReader(requestData));
Element body = doc.getRootElement().element("body");
Element beans = body.element("beans");
Element msgs = body.element("messages");
if (beans == null) {
log.debug("Recieved envelope is empty! [no 'beans' tag]");
return; /////There is nothing to handle...
}
String[] loadedMsgs = msgs != null ? msgs.getText().split(",,") : null;
Iterator<Element> children = beans.elementIterator();
Locale locale = null;
try {
Bean<Locale> bean = (Bean<Locale>) beanManager.getBeans(Locale.class).iterator().next();
locale = bean.create(beanManager.createCreationalContext(bean));
} catch (Exception exp) {
////// for some reason, which most likely would be not having International Module being used,
/////// we cannot get hold of the dynamic Locale instance, so we have to carry on with the server's default one
locale = Locale.getDefault();
}
this.marshalResponse(children, loadedMsgs, response.getOutputStream(), locale);
}
/*
* convert and then write bean's constraint metadata into the response stream,
* it also returns a list of required validation messages
*/
protected Map<String, Object[]> translateConstraints(String beanName, String qualifiers, OutputStream out)
throws IOException {
Bean<?> targetBean = getTargetBean(beanName, qualifiers);
HashMap<String, Object[]> validationMessages = new HashMap<String, Object[]>(32);
HashMap<String, ArrayList<String>> groupHierarechy = new HashMap<String, ArrayList<String>>();
out.write(BEAN_TAG_OPEN_START);
out.write(beanName.getBytes());
out.write(BEAN_TAG_OPEN_END);
Validator validator = Factory.getValidator();
BeanDescriptor descriptor = validator.getConstraintsForClass(targetBean.getBeanClass());
Iterator<PropertyDescriptor> descriptors = descriptor.getConstrainedProperties().iterator();
while (descriptors.hasNext()) {
PropertyDescriptor prop = descriptors.next();
out.write(PROPERTY_TAG_OPEN_START);
out.write(prop.getPropertyName().getBytes());
out.write(PROPERTY_TAG_OPEN_END);
Iterator<ConstraintDescriptor<?>> constraints = prop.getConstraintDescriptors().iterator();
while (constraints.hasNext()) {
ConstraintDescriptor<?> constraint = constraints.next();
String[] outcome = convertConstraint(constraint, groupHierarechy, out, null);
validationMessages.put(outcome[0], new Object[]{constraint, outcome[1]});
}
out.write(PROPERTY_TAG_CLOSE);
}
if (groupHierarechy.size() > 0) {
Iterator<String> keys = groupHierarechy.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
String[] tokens = key.split(":"); ///// first token is name , second one is id
out.write((GROUP_HIERARCHY_TAG_OPEN + tokens[1]).getBytes());
out.write((GROUP_HIERARCHY_TAG_MIDDLE + tokens[0]).getBytes());
out.write(GROUP_HIERARCHY_TAG_OPEN_END.getBytes());
ArrayList<String> parentsTags = groupHierarechy.get(key);
if (parentsTags.size() > 0) {
for (String tag : parentsTags)
out.write(tag.getBytes());
}
out.write(GROUP_HIERARCHY_CLOSE.getBytes());
}
}
out.write(BEAN_TAG_CLOSE);
return validationMessages;
}
/*
* The core functionality
*/
protected String[] convertConstraint(ConstraintDescriptor<?> constraint,
HashMap<String, ArrayList<String>> gh,
OutputStream out,
String parent)
throws IOException {
String annotation = constraint.getAnnotation().annotationType().toString();
annotation = annotation.substring(annotation.lastIndexOf(".") + 1); ////I decided not to use the fully-qualified name
Map<String, Object> attrs = constraint.getAttributes(); //// in order to reduce the amount of data that is sent to client
HashMap<String, Object> params = null;
if (attrs != null) {
params = new HashMap<String, Object>();
for (String attr : attrs.keySet()) {
if (!DEFAULT_ATTRIBUTES.contains(attr))
params.put(attr, attrs.get(attr));
}
}
if (this.specialConsiderations.containsKey(annotation)) {
SpecialConsideration sc = this.specialConsiderations.get(annotation);
annotation = sc.reassessConstraintName(annotation);
params = sc.reassessParameters(params);
}
out.write(CONSTRAINT_TAG_OPEN_START);
out.write(annotation.getBytes());
if (parent != null) {
out.write(CONSTRAINT_TAG_MIDDLE);
out.write(parent.getBytes());
}
out.write(CONSTRAINT_TAG_OPEN_END);
///////////////////////// Handling Groups if there is any
if (gh != null) {
Set<Class<?>> groups = constraint.getGroups();
if (groups.size() > 0) {
int counter = gh.size();
Iterator<Class<?>> groupIter = groups.iterator();
while (groupIter.hasNext()) {
Class<?> groupClass = groupIter.next();
String name = groupClass.getName();
if (!name.equals("javax.validation.groups.Default")) {
out.write(GROUP_TAG_OPEN);
out.write((BuildGroupHierarchy(groupClass, ++counter, gh) + "").getBytes());
out.write(GROUP_TAG_CLOSE);
}
}
}
}
///////////////////////////////////////////////////////
if (params != null && params.size() > 0) {
StringBuilder builder = new StringBuilder("_[");
for (String key : params.keySet()) {
Object obj = params.get(key);
builder.append(key).append(":");
out.write(PARAMETER_TAG_OPEN_START);
out.write(key.getBytes());
out.write(PARAMETER_TAG_OPEN_MIDDLE);
String JSValue = obj.toString();
if (obj.getClass().isArray())
JSValue = JsConverter.convertArray((Object[]) obj);
else if (obj instanceof Collection<?>)
JSValue = JsConverter.convertCollection((Collection<?>) obj);
else if (obj instanceof Map)
JSValue = JsConverter.convertMap((Map<?, ?>) obj);
out.write(JSValue.getBytes());
out.write(PARAMETER_TAG_OPEN_END);
builder.append(JSValue).append(",");
}
builder.deleteCharAt(builder.length() - 1);
builder.append("]");
annotation += builder.toString();
}
out.write(CONSTRAINT_TAG_CLOSE);
Set<ConstraintDescriptor<?>> compositeConstraints = constraint.getComposingConstraints();
if (compositeConstraints != null) {
Iterator<ConstraintDescriptor<?>> composites = compositeConstraints.iterator();
while (composites.hasNext()) {
convertConstraint(composites.next(), null, out, annotation);
}
} ///////////the outcome of all composed constraints is ignored, since their message
////////// will be overidden by their parent's anyway...
return parent == null ? new String[]{annotation, (String) attrs.get("message")} : null;
}
/*
* extract the actual Bean
*/
protected Bean<?> getTargetBean(String beanName, String qualifiers) {
Bean<?> targetBean = null;
Set<Bean<?>> beans = beanManager.getBeans(beanName);
if (beans.isEmpty()) {
try {
Class<?> beanType = Class.forName(beanName);
Annotation[] q = qualifiers != null && !Strings.isEmpty(qualifiers) ?
new AnnotationsParser(beanType, qualifiers, beanManager).getAnnotations() :
EMPTY_ANNOTATIONS;
beans = beanManager.getBeans(beanType, q);
} catch (ClassNotFoundException ex) {
throw new IllegalArgumentException("Invalid bean class specified: " + beanName);
}
if (beans.isEmpty()) {
throw new IllegalArgumentException(
"Could not find bean with bean with type/name " + beanName +
", qualifiers [" + qualifiers + "]");
}
}
targetBean = beans.iterator().next();
return targetBean;
}
/*
* response is made here by converting each bean's constraint metadata and sending both metadata and required
* messages.
*
*/
private void marshalResponse(Iterator<Element> beans,
String[] msgs,
OutputStream out,
Locale locale) throws IOException {
out.write(ENVELOPE_TAG_OPEN);
out.write(BODY_TAG_OPEN);
out.write(VALIDATION_TAG_OPEN);
if (beans == null)
out.write("<null/>".getBytes());
else {
HashMap<String, Object[]> requiredMsgs = new HashMap<String, Object[]>();
while (beans.hasNext()) {
final Element bean = beans.next();
final Attribute qualifier = bean.attribute("qualifiers");
requiredMsgs.putAll(this.translateConstraints(bean.attribute("target").getText(),
qualifier != null ? qualifier.getText() : null,
out));
}
out.write(MESSAGES_TAG_OPEN);
if (msgs != null) {
for (String msg : msgs) /////cross-checking already available validation messages with
requiredMsgs.remove(msg); ///// the ones we are about to send
}
MessageInterpolator messageHandler = Factory.getMessageInterpolator();
for (String key : requiredMsgs.keySet()) {
out.write(MESSAGE_TAG_OPEN_START);
out.write(key.getBytes());
out.write(MESSAGE_TAG_OPEN_MIDDLE);
Object[] entry = requiredMsgs.get(key);
FakeInterpolatorContext context = new FakeInterpolatorContext((ConstraintDescriptor<?>) entry[0]);
out.write(messageHandler.interpolate((String) entry[1], context, locale)
.replace("\"", "'")
.replace("&", "&")
.replace("<", "<")
.getBytes());
out.write(MESSAGE_TAG_OPEN_END);
}
out.write(MESSAGES_TAG_CLOSE);
}
out.write(VALIDATION_TAG_CLOSE);
out.write(BODY_TAG_CLOSE);
out.write(ENVELOPE_TAG_CLOSE);
out.flush();
}
/*
* make the group hierarchy, while ensuring that there is no duplicate entry
*/
private int BuildGroupHierarchy(Class<?> group,
int counter,
final HashMap<String, ArrayList<String>> gh) {
String groupName = group.getName();
Iterator<String> keys = gh.keySet().iterator();
int flag = -1;
while (keys.hasNext()) {
String n = keys.next();
if (n.startsWith(groupName.substring(groupName.lastIndexOf(".") + 1))) {
flag = Integer.parseInt(n.split(":")[1]);
break;
}
}
if (flag != -1)
return flag;
Class<?>[] parents = group.getInterfaces();
ArrayList<String> tags = new ArrayList<String>();
for (int i = 0; i < parents.length; i++) {
Class<?>[] grandParents = parents[i].getInterfaces();
String name = parents[i].getName();
name = name.substring(name.lastIndexOf(".") + 1);
if (grandParents.length == 0)
tags.add(GROUP_PARENT_TAG_OPEN + GROUP_PARENT_TAG_MIDDLE_NAME + name + GROUP_PARENT_TAG_CLOSE);
else {
int temp = BuildGroupHierarchy(parents[i], counter + i + 1, gh);
tags.add(GROUP_PARENT_TAG_OPEN + GROUP_PARENT_TAG_MIDDLE_ID + temp + GROUP_PARENT_TAG_CLOSE);
}
}
gh.put(groupName.substring(groupName.lastIndexOf(".") + 1) + ":" + counter, tags);
return counter;
}
/*
* we need a InterpolatorContext to fetch messages before really validating them.
*/
private final class FakeInterpolatorContext implements MessageInterpolator.Context {
private ConstraintDescriptor<?> cd;
public FakeInterpolatorContext(ConstraintDescriptor<?> descriptor) {
this.cd = descriptor;
}
public ConstraintDescriptor<?> getConstraintDescriptor() {
return this.cd;
}
public Object getValidatedValue() {
return "?value?"; //////we dont know yet!!
}
}
}