// A and (B or C)
criteriaFor(Person.class).withProperty(aaa()).eq("A").and().lbrace()
.withProperty(bbb()).eq("B").or().withProperty(ccc()).eq("C").rbrace()
.build();
That should build a syntax tree like:
and
/ \
A or
/ \
B C
This means that I need to hold state of current and previous not completed operators in the builder. I hold the current operator in a stack and give braces a special meaning. The expression builder looks like this:
public class ConditionalCriteriaBuilder<T> {
private enum ExpressionOperator {
Not, Or, And, LBrace
}
private ConditionalCriteriaBuilder() {
}
public static <T> ConditionRoot<T> criteriaFor(Class<T> clazz) {
return new ConditionalCriteriaBuilder<T>().new RootBuilderImpl();
}
public interface ConditionRoot<T> {
List<ConditionalCriteria> build();
CondditionProperty<T> withProperty(Property<T> property);
ConditionRoot<T> and();
ConditionRoot<T> or();
ConditionRoot<T> not();
ConditionRoot<T> lbrace();
ConditionRoot<T> rbrace();
}
public interface CondditionProperty<T> {
ConditionRoot<T> eq(Object value);
}
public class RootBuilderImpl implements ConditionRoot<T> {
private final SimpleStack<ConditionalCriteria> criteriaStack = new SimpleStack<ConditionalCriteria>();
private final SimpleStack<ExpressionOperator> operatorStack = new SimpleStack<ExpressionOperator>();
/**
* End the expression with this build
*/
public List<ConditionalCriteria> build() {
return criteriaStack.asList();
}
public CondditionProperty<T> withProperty(Property<T> property) {
return new PropBuilderImpl(property.getName());
}
public ConditionRoot<T> and() {
operatorStack.push(ExpressionOperator.And);
return this;
}
public ConditionRoot<T> or() {
operatorStack.push(ExpressionOperator.Or);
return this;
}
public ConditionRoot<T> not() {
operatorStack.push(ExpressionOperator.Not);
return this;
}
public ConditionRoot<T> lbrace() {
operatorStack.push(ExpressionOperator.LBrace);
return this;
}
public ConditionRoot<T> rbrace() {
operatorStack.pop();
if (criteriaStack.isEmpty()) {
return this;
}
ConditionalCriteria lastCriteria = popCriteria();
pushCriteria(lastCriteria);
return this;
}
private ConditionalCriteria popCriteria() {
return criteriaStack.pop();
}
private void pushCriteria(ConditionalCriteria criteria) {
ExpressionOperator currentOperator = operatorStack.peek();
if (currentOperator == ExpressionOperator.Or || currentOperator == ExpressionOperator.And) {
ConditionalCriteria compositeCriteria;
if (currentOperator == ExpressionOperator.Or) {
compositeCriteria = ConditionalCriteria.or(popCriteria(), criteria);
} else {
compositeCriteria = ConditionalCriteria.and(popCriteria(), criteria);
}
criteriaStack.push(compositeCriteria);
operatorStack.pop();
} else if (currentOperator == ExpressionOperator.Not) {
ConditionalCriteria notCriteria = ConditionalCriteria.not(criteria);
criteriaStack.push(notCriteria);
operatorStack.pop();
if (!operatorStack.isEmpty() && !criteriaStack.isEmpty()) {
pushCriteria(criteriaStack.pop());
}
} else if (currentOperator == ExpressionOperator.LBrace) {
criteriaStack.push(criteria);
} else {
criteriaStack.push(criteria);
}
}
private class PropBuilderImpl implements CondditionProperty<T> {
String propertyName;
PropBuilderImpl(String name) {
this.propertyName = name;
}
public ConditionRoot<T> eq(Object value) {
pushCriteria(ConditionalCriteria.equal(propertyName, value));
return RootBuilderImpl.this;
}
}
}
}
The magic is in pushCriteria, which is invoked at the end of a condition, such as
eq()
, and also when right brace is reached. It is not a trivial problem, but thanks to the stack metaphor the solution is not that complicated.A final thought about the syntax. Look at the expression again:
// A and (B or C)
criteriaFor(Person.class).withProperty(aaa()).eq("A").and().lbrace()
.withProperty(bbb()).eq("B").or().withProperty(ccc()).eq("C").rbrace()
.build();
I use special keywords for left brace and right brace. Why not use grouping with an single method instead and write the nested expression as method parameter?
// A and (B or C)
criteriaFor(Person.class).withProperty(aaa()).eq("A").and().group(
criteriaFor(Person.class).withProperty(bbb()).eq("B").or().withProperty(ccc()).eq("C"))
.buildSingle();
That would be possible, and probably easier to implement, but I see a few problems from end user perspective:
- There are already much clutter of braces due to the methods and it is hard to see and write the end brace of the group operation.
- You loose context and have to start the nested expression from scratch, i.e. with static criteriaFor method.
That was all of this exercise. I hope you find it useful as a practical example of how to develop a small Internal DSL. You find the full source code in subversion, including JUnit test.
Nice tutorial. See here for a typesafe DSL for Criteria like queries : http://blog.mysema.com/2010/07/querying-hibernate-with-querydsl.html
ReplyDeleteQuerydsl uses a fluent Query interface and code generation to mirror the domain model to a Query model. The syntax is JPQL-like, typesafe and very compact.