After having no actual solution as replies, I created a proxy class to handle these situations. The idea is to retain native syntax where possible.
WARNING: This is work-in-progress and very hazardous approach. The code below is by no mean meant as complete solution and quite possibly contains zillions of bugs and scary cases.
That being said, the BlankAwareQuery class wraps the javax.persistence Query and is initialized with EntityManager and the core query string (which cannot contain empty lists or lists of enumerations).
BlankAwareQuery query = new BlankAwareQuery(em, "SELECT p FROM Profile p");
After creation of class, dynamic parts are inserted with
query.from("p.address a");
query.where("a IN (:addresses)");
Parameters are inserted as always:
query.setParameter("addresses", addresses);
The point here is that the class removes these (their from-part as well) from query if they are empty lists or manipulates them if they are lists of enumerations.
Then call:
query.getResultList();
So, for example:
List<Profile> profiles = new BlankAwareQuery(em, "SELECT p FROM Profile p")
.from("p.address a JOIN a.municipality m").where("m IN (:municipalities)")
.where("p.gender IN (:genders)")
.where("p.yearOfBirth > :minYear")
.where("p.yearOfBirth < :maxYear")
.from("p.platforms f").where("f IN (:platforms)")
.setParameter("municipalities", municipalities)
.setParameter("genders", genders)
.setParameter("minYear", minYear)
.setParameter("maxYear", maxYear)
.setParameter("platforms", platforms)
.getResultList();
The actual code (code uses Lombok for @Data and @NonNull annotations and Apache commons lang for StringUtils):
public class BlankAwareQuery {
private @Data class Parameter {
private @NonNull String fieldName;
private @NonNull Object value;
}
private @Data class ClausePair {
private @NonNull String from;
private @NonNull String where;
}
private EntityManager em;
private List<String> select = Lists.newArrayList();
private List<ClausePair> whereFrom = Lists.newArrayList();
private String from;
private List<Parameter> parameters = Lists.newArrayList();
Query query;
public BlankAwareQuery(EntityManager em, String query) {
this.em = em;
/** Select **/
int selectStart = StringUtils.indexOf(query, "SELECT ") + 7;
int selectEnd = StringUtils.indexOf(query, " FROM ");
select(StringUtils.substring(query, selectStart, selectEnd));
/** From **/
int fromStart = selectEnd + 6;
int fromEnd = StringUtils.indexOf(query, " WHERE ");
if (fromEnd == -1) fromEnd = query.length();
from(StringUtils.substring(query, fromStart, fromEnd));
/** Where **/
String where = "";
if (StringUtils.contains(query, " WHERE ")) {
where = StringUtils.substring(query, fromEnd + 7);
}
where(where);
}
private BlankAwareQuery select(String s) {
select.add(s);
return this;
}
public BlankAwareQuery from(String s) {
from = s;
return this;
}
public BlankAwareQuery where(String s) {
ClausePair p = new ClausePair(from, s);
whereFrom.add(p);
from = "";
return this;
}
public BlankAwareQuery setParameter(String fieldName, Object value) {
/** Non-empty collection -> include **/
if (value != null && value instanceof List<?> && !((List<?>) value).isEmpty()) {
/** List of enums -> parse open (JPA doesn't support defining list of enums as in (:blaa) **/
if (((List<?>) value).get(0) instanceof Enum<?>) {
List<String> fields = Lists.newArrayList();
/** Split parameters into individual entries **/
int i = 0;
for (Enum<?> g : (List<Enum<?>>) value) {
String fieldSingular = StringUtils.substring(fieldName, 0, fieldName.length() - 1) + i;
fields.add(":" + fieldSingular);
parameters.add(new Parameter(fieldSingular, g));
i++;
}
/** Split :enums into (:enum1, :enum2, :enum3) strings **/
for (ClausePair p : whereFrom) {
if (p.getWhere().contains(":" + fieldName)) {
int start = StringUtils.indexOf(p.getWhere(), ":" + fieldName);
int end = StringUtils.indexOfAny(StringUtils.substring(p.getWhere(), start + 1), new char[] {')', ' '});
String newWhere = StringUtils.substring(p.getWhere(), 0, start) + StringUtils.join(fields, ", ") + StringUtils.substring(p.getWhere(), end + start + 1);
p.setWhere(newWhere);
}
}
}
/** Normal type which doesn't require customization, just add it **/
else {
parameters.add(new Parameter(fieldName, value));
}
}
/** Not to be included -> remove from and where pair from query **/
else {
for (Iterator<ClausePair> it = whereFrom.iterator(); it.hasNext();) {
ClausePair p = it.next();
if (StringUtils.contains(p.getWhere(), fieldName)) {
it.remove();
}
}
}
return this;
}
private String buildQueryString() {
List<String> from = Lists.newArrayList();
List<String> where = Lists.newArrayList();
for (ClausePair p : whereFrom) {
if (!p.getFrom().equals("")) from.add(p.getFrom());
if (!p.getWhere().equals("")) where.add(p.getWhere());
}
String selectQuery = StringUtils.join(select, ", ");
String fromQuery = StringUtils.join(from, " JOIN ");
String whereQuery = StringUtils.join(where, " AND ");
String query = "SELECT " + selectQuery + " FROM " + fromQuery + (whereQuery == "" ? "" : " WHERE " + whereQuery);
return query;
}
public Query getQuery() {
query = em.createQuery(buildQueryString());
setParameters();
return query;
}
private void setParameters() {
for (Parameter par : parameters) {
query.setParameter(par.getFieldName(), par.getValue());
}
}
public List getResultList() {
return getQuery().getResultList();
}
public Object getSingleResult() {
return getQuery().getSingleResult();
}
}