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.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!!
}
}
}