blob: 61ce1ba97b491744519007563aadf96566c5fdf0 [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.olingo.odata2.jpa.processor.core.access.data;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.uri.UriInfo;
import org.apache.olingo.odata2.api.uri.info.DeleteUriInfo;
import org.apache.olingo.odata2.api.uri.info.GetEntityCountUriInfo;
import org.apache.olingo.odata2.api.uri.info.GetEntitySetCountUriInfo;
import org.apache.olingo.odata2.api.uri.info.GetEntitySetUriInfo;
import org.apache.olingo.odata2.api.uri.info.GetEntityUriInfo;
import org.apache.olingo.odata2.api.uri.info.PutMergePatchUriInfo;
import org.apache.olingo.odata2.jpa.processor.api.ODataJPAContext;
import org.apache.olingo.odata2.jpa.processor.api.ODataJPAQueryExtensionEntityListener;
import org.apache.olingo.odata2.jpa.processor.api.ODataJPATombstoneEntityListener;
import org.apache.olingo.odata2.jpa.processor.api.exception.ODataJPAModelException;
import org.apache.olingo.odata2.jpa.processor.api.exception.ODataJPARuntimeException;
import org.apache.olingo.odata2.jpa.processor.api.jpql.JPQLContext;
import org.apache.olingo.odata2.jpa.processor.api.jpql.JPQLContextType;
import org.apache.olingo.odata2.jpa.processor.api.jpql.JPQLStatement;
import org.apache.olingo.odata2.jpa.processor.api.model.JPAEdmMapping;
public class JPAQueryBuilder {
enum UriInfoType {
GetEntitySet,
GetEntity,
GetEntitySetCount,
GetEntityCount,
PutMergePatch,
Delete
}
private EntityManager em = null;
private int pageSize = 0;
public JPAQueryBuilder(ODataJPAContext odataJPAContext) {
this.em = odataJPAContext.getEntityManager();
this.pageSize = odataJPAContext.getPageSize();
}
public JPAQueryInfo build(GetEntitySetUriInfo uriInfo) throws ODataJPARuntimeException {
JPAQueryInfo queryInfo = new JPAQueryInfo();
Query query = null;
try {
ODataJPATombstoneEntityListener listener = getODataJPATombstoneEntityListener((UriInfo) uriInfo);
if (listener != null) {
query = listener.getQuery(uriInfo, em);
}
if (query == null) {
query = buildQuery((UriInfo) uriInfo, UriInfoType.GetEntitySet);
} else {
queryInfo.setTombstoneQuery(true);
}
} catch (Exception e) {
throw ODataJPARuntimeException.throwException(
ODataJPARuntimeException.ERROR_JPQL_QUERY_CREATE, e);
}
queryInfo.setQuery(query);
return queryInfo;
}
public Query build(GetEntityUriInfo uriInfo) throws ODataJPARuntimeException {
Query query = null;
try {
ODataJPAQueryExtensionEntityListener listener = getODataJPAQueryEntityListener((UriInfo) uriInfo);
if (listener != null) {
query = listener.getQuery(uriInfo, em);
}
if (query == null) {
query = buildQuery((UriInfo) uriInfo, UriInfoType.GetEntity);
}
} catch (Exception e) {
throw ODataJPARuntimeException.throwException(
ODataJPARuntimeException.ERROR_JPQL_QUERY_CREATE, e);
}
return query;
}
public Query build(GetEntitySetCountUriInfo uriInfo) throws ODataJPARuntimeException {
Query query = null;
try {
ODataJPAQueryExtensionEntityListener listener = getODataJPAQueryEntityListener((UriInfo) uriInfo);
if (listener != null) {
query = listener.getQuery(uriInfo, em);
}
if (query == null) {
query = buildQuery((UriInfo) uriInfo, UriInfoType.GetEntitySetCount);
}
} catch (Exception e) {
throw ODataJPARuntimeException.throwException(
ODataJPARuntimeException.ERROR_JPQL_QUERY_CREATE, e);
}
return query;
}
public Query build(GetEntityCountUriInfo uriInfo) throws ODataJPARuntimeException {
Query query = null;
try {
ODataJPAQueryExtensionEntityListener listener = getODataJPAQueryEntityListener((UriInfo) uriInfo);
if (listener != null) {
query = listener.getQuery(uriInfo, em);
}
if (query == null) {
query = buildQuery((UriInfo) uriInfo, UriInfoType.GetEntityCount);
}
} catch (Exception e) {
throw ODataJPARuntimeException.throwException(
ODataJPARuntimeException.ERROR_JPQL_QUERY_CREATE, e);
}
return query;
}
public Query build(DeleteUriInfo uriInfo) throws ODataJPARuntimeException {
Query query = null;
try {
ODataJPAQueryExtensionEntityListener listener = getODataJPAQueryEntityListener((UriInfo) uriInfo);
if (listener != null) {
query = listener.getQuery(uriInfo, em);
}
if (query == null) {
query = buildQuery((UriInfo) uriInfo, UriInfoType.Delete);
}
} catch (Exception e) {
throw ODataJPARuntimeException.throwException(
ODataJPARuntimeException.ERROR_JPQL_QUERY_CREATE, e);
}
return query;
}
public Query build(PutMergePatchUriInfo uriInfo) throws ODataJPARuntimeException {
Query query = null;
try {
ODataJPAQueryExtensionEntityListener listener = getODataJPAQueryEntityListener((UriInfo) uriInfo);
if (listener != null) {
query = listener.getQuery(uriInfo, em);
}
if (query == null) {
query = buildQuery((UriInfo) uriInfo, UriInfoType.PutMergePatch);
}
} catch (Exception e) {
throw ODataJPARuntimeException.throwException(
ODataJPARuntimeException.ERROR_JPQL_QUERY_CREATE, e);
}
return query;
}
private Query buildQuery(UriInfo uriParserResultView, UriInfoType type)
throws EdmException,
ODataJPAModelException, ODataJPARuntimeException {
JPQLContextType contextType = determineJPQLContextType(uriParserResultView, type);
JPQLContext jpqlContext = buildJPQLContext(contextType, uriParserResultView);
JPQLStatement jpqlStatement = JPQLStatement.createBuilder(jpqlContext).build();
return em.createQuery(normalizeMembers(em, jpqlStatement.toString()));
}
public ODataJPAQueryExtensionEntityListener getODataJPAQueryEntityListener(UriInfo uriInfo) throws EdmException,
InstantiationException, IllegalAccessException {
ODataJPAQueryExtensionEntityListener queryListener = null;
ODataJPATombstoneEntityListener listener = getODataJPATombstoneEntityListener(uriInfo);
if (listener instanceof ODataJPAQueryExtensionEntityListener) {
queryListener = (ODataJPAQueryExtensionEntityListener) listener;
}
return queryListener;
}
public ODataJPATombstoneEntityListener getODataJPATombstoneEntityListener(UriInfo uriParserResultView)
throws InstantiationException, IllegalAccessException, EdmException {
JPAEdmMapping mapping = (JPAEdmMapping) uriParserResultView.getTargetEntitySet().getEntityType().getMapping();
if (mapping.getODataJPATombstoneEntityListener() != null) {
return (ODataJPATombstoneEntityListener) mapping.getODataJPATombstoneEntityListener().newInstance();
}
return null;
}
public JPQLContext buildJPQLContext(JPQLContextType contextType, UriInfo uriParserResultView)
throws ODataJPAModelException, ODataJPARuntimeException {
if (pageSize > 0 && (contextType == JPQLContextType.SELECT || contextType == JPQLContextType.JOIN)) {
return JPQLContext.createBuilder(contextType, uriParserResultView, true).build();
} else {
return JPQLContext.createBuilder(contextType, uriParserResultView).build();
}
}
public JPQLContextType determineJPQLContextType(UriInfo uriParserResultView, UriInfoType type) {
JPQLContextType contextType = null;
if (uriParserResultView.getNavigationSegments().size() > 0) {
if (type == UriInfoType.GetEntitySet) {
contextType = JPQLContextType.JOIN;
} else if (type == UriInfoType.Delete || type == UriInfoType.Delete || type == UriInfoType.GetEntity
|| type == UriInfoType.PutMergePatch) {
contextType = JPQLContextType.JOIN_SINGLE;
} else if (type == UriInfoType.GetEntitySetCount || type == UriInfoType.GetEntityCount) {
contextType = JPQLContextType.JOIN_COUNT;
}
} else {
if (type == UriInfoType.GetEntitySet) {
contextType = JPQLContextType.SELECT;
} else if (type == UriInfoType.Delete || type == UriInfoType.GetEntity
|| type == UriInfoType.PutMergePatch) {
contextType = JPQLContextType.SELECT_SINGLE;
} else if (type == UriInfoType.GetEntitySetCount || type == UriInfoType.GetEntityCount) {
contextType = JPQLContextType.SELECT_COUNT;
}
}
return contextType;
}
private static final Pattern NORMALIZATION_NEEDED_PATTERN = Pattern.compile(".*[\\s(](\\S+\\.\\S+\\.\\S+).*");
private static final Pattern VALUE_NORM_PATTERN = Pattern.compile("(?:^|\\s|\\()'(([^']*)')");
private static final Pattern JOIN_ALIAS_PATTERN = Pattern.compile(".*\\sJOIN\\s(\\S*\\s\\S*).*");
private static String normalizeMembers(EntityManager em, String jpqlQuery) {
//check if clause values are string with x.y.z format
//starting with quotes;
String query = checkConditionValues(jpqlQuery);
// check if normalization is needed (if query contains "x.y.z" elements
// starting with space or parenthesis)
Matcher normalizationNeededMatcher = NORMALIZATION_NEEDED_PATTERN.matcher(query);
if (!normalizationNeededMatcher.find()) {
return jpqlQuery;
}
if (containsEmbeddedAttributes(em, jpqlQuery)) {
return jpqlQuery;
}
String normalizedJpqlQuery = jpqlQuery;
Map<String, String> joinAliases = new HashMap<String, String>();
// collect information about existing joins/aliases
Matcher joinAliasMatcher = JOIN_ALIAS_PATTERN.matcher(normalizedJpqlQuery);
if (joinAliasMatcher.find()) {
for (int i = 1; i <= joinAliasMatcher.groupCount(); i++) {
String[] joinAlias = joinAliasMatcher.group(i).split(String.valueOf(JPQLStatement.DELIMITER.SPACE));
joinAliases.put(joinAlias[0], joinAlias[1]);
}
}
// normalize query
boolean normalizationNeeded = true;
while (normalizationNeeded) {
String membershipToNormalize = normalizationNeededMatcher.group(1);
// get member info
String memberInfo = membershipToNormalize.substring(0,
ordinalIndexOf(membershipToNormalize, JPQLStatement.DELIMITER.PERIOD, 1));
String alias;
if (joinAliases.containsKey(memberInfo)) {
// use existing alias
alias = joinAliases.get(memberInfo);
} else {
// create new join/alias
alias = "R" + (joinAliases.size() + 1);
int joinInsertPosition = normalizedJpqlQuery.indexOf(JPQLStatement.KEYWORD.WHERE);
if (joinInsertPosition == -1) {
joinInsertPosition = normalizedJpqlQuery.indexOf(JPQLStatement.KEYWORD.ORDERBY);
}
normalizedJpqlQuery = normalizedJpqlQuery.substring(0, joinInsertPosition) + JPQLStatement.KEYWORD.JOIN
+ JPQLStatement.DELIMITER.SPACE + memberInfo + JPQLStatement.DELIMITER.SPACE + alias
+ JPQLStatement.DELIMITER.SPACE + normalizedJpqlQuery.substring(joinInsertPosition);
joinAliases.put(memberInfo, alias);
}
// use alias
normalizedJpqlQuery = normalizedJpqlQuery.replaceAll(memberInfo + "\\" + JPQLStatement.DELIMITER.PERIOD,
alias + JPQLStatement.DELIMITER.PERIOD);
//check for values like "x.y.z"
query = checkConditionValues(normalizedJpqlQuery);
// check if further normalization is needed
normalizationNeededMatcher = NORMALIZATION_NEEDED_PATTERN.matcher(query);
normalizationNeeded = normalizationNeededMatcher.find();
}
// add distinct to avoid duplicates in result set
return normalizedJpqlQuery.replaceFirst(
JPQLStatement.KEYWORD.SELECT + JPQLStatement.DELIMITER.SPACE,
JPQLStatement.KEYWORD.SELECT_DISTINCT + JPQLStatement.DELIMITER.SPACE);
}
/**
* Check if the statement contains string values having x.y.z kind of format
* It will replace those values with parameters before checking for normalization
* and later added back
* */
private static String checkConditionValues(String jpqlQuery) {
int i=0;
StringBuffer query= new StringBuffer();
query.append(jpqlQuery);
Matcher valueMatcher = VALUE_NORM_PATTERN.matcher(query);
while (valueMatcher.find()) {
String val = valueMatcher.group();
int index = query.indexOf(val);
String var = "["+ ++i +"] ";
query.replace(index, index + val.length(), var);
valueMatcher = VALUE_NORM_PATTERN.matcher(query);
}
return query.toString();
}
/**
* Verify via {@link EntityManager} if one of the attributes of the selected entity
* contains a embedded attribute.
* Return true if at least one embedded attribute is found or false if non embedded
* attribute is found.
*
* @param em according entity manager
* @param jpqlQuery query to verify
* @return true if at least one embedded attribute is found or false if non embedded
* attribute is found.
*/
private static boolean containsEmbeddedAttributes(EntityManager em, String jpqlQuery) {
Set<EntityType<?>> types = em.getMetamodel().getEntities();
int pos = jpqlQuery.indexOf("FROM ") + 5;
int lastpos = jpqlQuery.indexOf(" ", pos);
final String queriedEntity = jpqlQuery.substring(pos, lastpos);
for (EntityType<?> type : types) {
if(queriedEntity.equals(type.getName())) {
Set<Attribute<?, ?>> attributes = (Set<Attribute<?, ?>>) type.getAttributes();
for (Attribute<?, ?> attribute : attributes) {
if(jpqlQuery.contains(attribute.getName()) &&
attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) {
return true;
}
}
}
}
return false;
}
private static int ordinalIndexOf(String str, char s, int n) {
int pos = str.indexOf(s, 0);
while (n-- > 0 && pos != -1) {
pos = str.indexOf(s, pos + 1);
}
return pos;
}
final class JPAQueryInfo {
private Query query = null;
private boolean isTombstoneQuery = false;
public Query getQuery() {
return query;
}
public void setQuery(Query query) {
this.query = query;
}
public boolean isTombstoneQuery() {
return isTombstoneQuery;
}
public void setTombstoneQuery(boolean isTombstoneQuery) {
this.isTombstoneQuery = isTombstoneQuery;
}
}
}