/* * Copyright 2008-2011 the original author or authors. * * 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 com.nominanuda.dataobject.schema; import static com.nominanuda.dataobject.schema.JclLexer.ARRAY; import static com.nominanuda.dataobject.schema.JclLexer.ARRAYVAL; import static com.nominanuda.dataobject.schema.JclLexer.ENTRY; import static com.nominanuda.dataobject.schema.JclLexer.ENTRYSEQ; import static com.nominanuda.dataobject.schema.JclLexer.OBJECT; import static com.nominanuda.dataobject.schema.JclLexer.PRIMITIVE; import static com.nominanuda.dataobject.schema.JclLexer.TYPEDEF; import static com.nominanuda.dataobject.schema.JclLexer.TYPEREF; import static com.nominanuda.dataobject.schema.JclLexer.VALUESEQ; import java.io.Reader; import java.io.StringReader; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Stack; import java.util.function.Function; import org.antlr.runtime.ANTLRReaderStream; import org.antlr.runtime.CharStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.tree.CommonTree; import org.antlr.runtime.tree.Tree; import com.nominanuda.code.Nullable; import com.nominanuda.dataobject.WrappingRecognitionException; import com.nominanuda.dataobject.transform.BaseJsonTransformer; import com.nominanuda.dataobject.transform.JsonTransformer; import com.nominanuda.lang.Check; import com.nominanuda.lang.ObjectFactory; public class JclValidatorFactory { private CommonTree root; private PrimitiveValidatorFactory primitiveValidatorFactory = new DefaultPrimitiveValidatorFactory(); private final LinkedHashMap<String, Tree> tMap = new LinkedHashMap<String, Tree>(); public JclValidatorFactory(String jclExpr) throws Exception { this(new StringReader(jclExpr)); } public JclValidatorFactory(Reader jclExpr) throws Exception { try { CharStream cs = new ANTLRReaderStream(jclExpr); JclLexer lexer = new JclLexer(cs); CommonTokenStream tokens = new CommonTokenStream(lexer); JclParser parser = new JclParser(tokens); CommonTree tree = (CommonTree)parser.program().getTree(); root = tree; Tree cur = root; if(cur.getType() == TYPEDEF) { String tname = cur.getChild(0).getText(); tMap.put(tname, cur.getChild(1)); tMap.put("", cur.getChild(1)); } else if(cur.getText() != null) { tMap.put("", cur); } else { int len = cur.getChildCount(); boolean defaultAdded = false; for(int i = 0; i < len; i++) { Tree t = cur.getChild(i); Check.illegalstate.assertEquals(TYPEDEF, t.getType()); String tname = t.getChild(0).getText(); tMap.put(tname, t.getChild(1)); if(!defaultAdded) { tMap.put("", t.getChild(1)); defaultAdded = true; } } } } catch(WrappingRecognitionException e) { throw e.getWrappedException(); } } public ObjectFactory<JsonTransformer> buildValidatorFactory(@Nullable String type) { String t = Check.ifNull(type, ""); final Tree tree = tMap.get(t); return new ObjectFactory<JsonTransformer>() { public JsonTransformer getObject() { return makeValueTransformer(tree); } }; } public JsonTransformer buildValidator(@Nullable String type) { String t = Check.ifNull(type, ""); Tree tree = tMap.get(t); return makeValueTransformer(tree); } private JsonTransformer makeValueTransformer(Tree cur) { final Stack<EventConsumer> stack = new Stack<EventConsumer>(); stack.push(makeConsumer(cur, stack)); return new BaseJsonTransformer() { public boolean startObjectEntry(String key) throws RuntimeException { stack.peek().startObjectEntry(key); return super.startObjectEntry(key); } public boolean startObject() throws RuntimeException { stack.peek().startObject(); return super.startObject(); } public void startJSON() throws RuntimeException { super.startJSON(); } public boolean startArray() throws RuntimeException { stack.peek().startArray(); return super.startArray(); } public boolean primitive(Object value) throws RuntimeException { stack.peek().primitive(value); return super.primitive(value); } public boolean endObjectEntry() throws RuntimeException { stack.peek().endObjectEntry(); return super.endObjectEntry(); } public boolean endObject() throws RuntimeException { stack.peek().endObject(); return super.endObject(); } public void endJSON() throws RuntimeException { super.endJSON(); } public boolean endArray() throws RuntimeException { stack.peek().endArray(); return super.endArray(); } }; } private EventConsumer makeConsumer(Tree node, Stack<EventConsumer> stack) { switch (node.getType()) { case OBJECT: Map<String, EventConsumer> entryConsumers = new LinkedHashMap<String, EventConsumer>(); for(int i = 0; i < node.getChildCount(); i++) { Tree entry = node.getChild(i); if(entry.getType() == ENTRY) { EventConsumer c = makeConsumer(entry.getChild(2), stack); c.setPredicate(tokenToPredicate(entry.getChild(1))); String key = entry.getChild(0).getChild(0).getText(); entryConsumers.put(key, c); } else if(entry.getType() == ENTRYSEQ) { entryConsumers.put("*", new EntrySeqConsumer(stack)); } else { Check.illegalstate.fail(); } } return new ObjectConsumer(stack, entryConsumers); case ARRAY: List<EventConsumer> elementsConsumers = new LinkedList<EventConsumer>(); for(int i = 0; i < node.getChildCount(); i++) { Tree value = node.getChild(i); if(value.getType() == VALUESEQ) { elementsConsumers.add(new ValueSeqConsumer(stack)); } else { elementsConsumers.add(makeConsumer(value, stack)); } } return new ArrayConsumer(stack, elementsConsumers); case PRIMITIVE: return new PrimitiveConsumer(stack, makePrimitiveValidator(node)); case ARRAYVAL: Tree value = node.getChild(0); Tree pred = node.getChild(1); EventConsumer c = makeConsumer(value, stack); c.setPredicate(tokenToPredicate(pred)); return c; case TYPEREF: String tname = node.getChild(0).getText(); Tree typedef = Check.illegalargument.assertNotNull( tMap.get(tname), "type not found "+tname); return makeConsumer(typedef, stack); default: return Check.illegalstate.fail(); } } private ExistentialPredicate tokenToPredicate(Tree predTnkNode) { String p = predTnkNode.getChildCount() > 0 ? predTnkNode.getChild(0).getText() : null; return new ExistentialPredicate(p); } private Function<Object, String> makePrimitiveValidator(Tree node) { Tree t = node.getChild(0); String valDef = t == null ? DefaultPrimitiveValidatorFactory.ANYPRIMITIVE : t.getText(); return primitiveValidatorFactory.create(valDef); } //////////////////////////////////////// public class ObjectConsumer extends EventConsumer { private final Map<String,EventConsumer> entryConsumers; public ObjectConsumer(Stack<EventConsumer> stack, Map<String,EventConsumer> entryConsumers) { super(stack); this.entryConsumers = entryConsumers; } @Override public boolean startObject() throws RuntimeException { return true; } @Override public boolean endObject() throws RuntimeException { validateExit(); pop(); return true; } @Override public boolean primitive(Object value) throws RuntimeException { if(value == null) { if(! isNullable()) { throw new ValidationException("null value of not-nul property"); } else { return true; } } else { throw new ValidationException("found "+value.toString()+" where object expected"); } } private void validateExit() { for(Entry<String,EventConsumer> e : entryConsumers.entrySet()) { String k = e.getKey(); EventConsumer c = e.getValue(); if(! isEntrySequence(k)) { if(! c.isOptional()) { throw new ValidationException("missing property:"+k); } } } } private boolean isEntrySequence(String k) { return "*".equals(k); } @Override public boolean startObjectEntry(String key) throws RuntimeException { EventConsumer c = entryConsumers.remove(key); if(c == null && entryConsumers.containsKey("*")) { c = entryConsumers.get("*"); } if(c == null) { throw new ValidationException("unexpected entry with key "+key); } push(Check.notNull(c)); return true; } @Override public boolean endObjectEntry() throws RuntimeException { return true; } } public class ArrayConsumer extends EventConsumer { private final List<EventConsumer> elementsConsumers; private int i = 0; private boolean startArraySeen = false; public ArrayConsumer(Stack<EventConsumer> stack, List<EventConsumer> elementsConsumers) { super(stack); this.elementsConsumers = elementsConsumers; } @Override public boolean primitive(Object value) throws RuntimeException { EventConsumer c = pushNextConsumer(); c.primitive(value); return true; } private EventConsumer pushNextConsumer() { EventConsumer c = nextConsumer(); if(c == null) { throw new ValidationException("wrong number of elements in array, at position "+i); } push(c); return c; } @Override public boolean startObject() throws RuntimeException { EventConsumer c = pushNextConsumer(); c.startObject(); return true; } @Override public boolean startArray() throws RuntimeException { if(startArraySeen) { EventConsumer c = pushNextConsumer(); c.startArray(); } else { startArraySeen = true; } return true; } @Override public boolean endArray() throws RuntimeException { pop(); if(i != elementsConsumers.size()) { throw new ValidationException("array expected "+elementsConsumers.size()+" elements, but got "+i); } return true; } private @Nullable EventConsumer nextConsumer() { if(i >= elementsConsumers.size()) { return null; } else { EventConsumer c = elementsConsumers.get(i); i++; return c; } } } public class EntrySeqConsumer extends EventConsumer { private int deep = 0; public EntrySeqConsumer(Stack<EventConsumer> stack) { super(stack); } public boolean startObject() throws RuntimeException { return true; } public boolean endObject() throws RuntimeException { return true; } public boolean startObjectEntry(String key) throws RuntimeException { deep++; return true; } public boolean endObjectEntry() throws RuntimeException { if(deep == 0) { pop(); } else { deep--; } return true; } public boolean startArray() throws RuntimeException { return true; } public boolean endArray() throws RuntimeException { return true; } public boolean primitive(Object value) throws RuntimeException { return true; } } public class ValueSeqConsumer extends EventConsumer { private int deep = 0; public ValueSeqConsumer(Stack<EventConsumer> stack) { super(stack); } public boolean startObject() throws RuntimeException { return true; } public boolean endObject() throws RuntimeException { return true; } public boolean startObjectEntry(String key) throws RuntimeException { return true; } public boolean endObjectEntry() throws RuntimeException { return true; } public boolean startArray() throws RuntimeException { deep++; return true; } public boolean endArray() throws RuntimeException { if(deep == 0) { pop(); } else { deep--; } return true; } public boolean primitive(Object value) throws RuntimeException { return true; } } public class PrimitiveConsumer extends EventConsumer { private final Function<Object, String> validator; public PrimitiveConsumer(Stack<EventConsumer> stack, Function<Object, String> validator) { super(stack); this.validator = validator; } public PrimitiveConsumer(Stack<EventConsumer> stack, Function<Object, String> validator, ExistentialPredicate pred) { super(stack, pred); this.validator = validator; } @Override public boolean primitive(Object value) throws RuntimeException { if(value == null) { if(! isNullable()) { throw new ValidationException("null value of not-nul property"); } else { pop(); return true; } } else { String errorMessage = validator.apply(value); if(errorMessage != null) { throw new ValidationException(errorMessage); } pop(); return true; } } } public void setPrimitiveValidatorFactory( PrimitiveValidatorFactory primitiveValidatorFactory) { this.primitiveValidatorFactory = primitiveValidatorFactory; } }