All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.sdk.odatav2.connectivity.ODataQuery Maven / Gradle / Ivy

There is a newer version: 1.40.11
Show newest version
/*******************************************************************************
 * (c) 201X SAP SE or an SAP affiliate company. All rights reserved.
 ******************************************************************************/
package com.sap.cloud.sdk.odatav2.connectivity;

import static com.sap.cloud.sdk.odatav2.connectivity.internal.ODataConnectivityUtil.SEPARATOR_PATH;
import static com.sap.cloud.sdk.odatav2.connectivity.internal.ODataConnectivityUtil.convertKeyValuesToString;
import static com.sap.cloud.sdk.odatav2.connectivity.internal.ODataConnectivityUtil.withSeparator;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.edm.EdmEntityType;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.edm.EdmNavigationProperty;
import org.apache.olingo.odata2.api.edm.EdmType;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.client.api.ODataClient;
import org.slf4j.Logger;

import com.google.common.collect.ImmutableList;
import com.sap.cloud.sdk.cloudplatform.cache.CacheKey;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.WithDestinationName;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.HttpClientInstantiationException;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder.ODataQueryResolver;
import com.sap.cloud.sdk.odatav2.connectivity.api.IODataQuery;
import com.sap.cloud.sdk.odatav2.connectivity.cache.metadata.GuavaMetadataCache;
import com.sap.cloud.sdk.odatav2.connectivity.cache.metadata.MetadataCache;

import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class ODataQuery implements IODataQuery{
	@SuppressWarnings("deprecation")
	private static final Logger logger = CloudLoggerFactory.getLogger(ODataQuery.class);
	private MetadataCache metadataCache = new GuavaMetadataCache();
	private static final List queryListeners;

	static {
		queryListeners = ImmutableList.copyOf(ServiceLoader.load(ODataQueryListener.class));
	}

	private static final String SEPARATOR_QUERY = "&";
	private static final String METADATA = "$metadata";

	@NonNull
	private final String servicePath;

	@NonNull
	private String entity;

	@Nullable
	private final Map keys;

	@NonNull
	private final ODataQueryBuilder.ODataQueryResolver oDataQueryResolver;

	@Nullable
	private final ErrorResultHandler errorHandler;

	@Nullable
	private final Map headers;

	@Nullable
	private final Map destinationRelevantHeaders;

	private final boolean useMetadata;

	private final Boolean cacheMetadata;
	
	private final URL metadataFliePath;
	
	private final CacheKey cacheKey;
	
	private boolean isCacheRefresh;
	
	private boolean isMediaRequest = false;
	private EdmEntitySet entitymetadata = null;
	private EdmEntityType entityType = null;
	private String requestQuery;
	private String requestLocalPath;
	private HttpResponse httpResponse = null;
	private static final String REQUESTFAILURE = "Failed to execute OData request.";
	private List navigationProperties = null;
	
	private EdmEntitySet lastEntityMetadata;
	
	public ODataQuery(String servicePath, String entity, Map keys,
			ODataQueryResolver oDataQueryResolver, ErrorResultHandler errorHandler, Map headers,
			Map destinationRelevantHeaders, boolean useMetadata, boolean cacheMetadata,
			URL metadataFilePath, CacheKey cacheKey, boolean isCacheRefresh) {
		this.servicePath = servicePath;
		this.entity = entity;
		this.keys = keys;
		this.oDataQueryResolver = oDataQueryResolver;
		this.errorHandler = errorHandler;
		this.headers = headers;
		this.destinationRelevantHeaders = destinationRelevantHeaders;
		this.useMetadata = useMetadata;
		this.cacheMetadata = cacheMetadata;
		this.metadataFliePath = metadataFilePath;
		this.cacheKey = cacheKey;
		this.isCacheRefresh = isCacheRefresh;
	}
	public ODataQuery(String servicePath, String entity, Map keys,
			ODataQueryResolver oDataQueryResolver, ErrorResultHandler errorHandler, Map headers,
			Map destinationRelevantHeaders, boolean useMetadata, boolean cacheMetadata,
			URL metadataFilePath, CacheKey cacheKey, boolean isCacheRefresh,
			List navigationProperties) {
		this(servicePath, entity, keys, oDataQueryResolver, errorHandler, headers,
		        destinationRelevantHeaders, useMetadata, cacheMetadata, metadataFilePath, cacheKey, 
		        isCacheRefresh);
		this.navigationProperties = navigationProperties;
	}

	/**
	 * Executes the OData query represented by this ODataQuery object.
	 * 
	 * @param providedClient
	 *            custom HttpClient capable of connecting to the data source
	 * @return ODataQueryResult represents the result of the query operation
	 * @throws ODataException
	 */
	public ODataQueryResult execute(HttpClient providedClient) throws ODataException {
		return execute(providedClient, false);
	}

	/**
	 * Executes the OData query represented by this ODataQuery object.
	 * 
	 * @param providedClient
	 *            HttpClient capable of connecting to the data source
	 * @param isMediaRequest
	 *            indicates if the request is for a media resource
	 * @return ODataQueryResult represents the result of the query operation
	 * @throws ODataException
	 */
	public ODataQueryResult execute(HttpClient providedClient, boolean isMediaRequest) throws ODataException {
		ODataQueryResult result = null;
		try {
			result = internalExecute(null, isMediaRequest, true, providedClient);
		} catch (ODataException e) {
			if (e.getODataExceptionType().equals(ODataExceptionType.OTHER)
					|| e.getODataExceptionType().equals(ODataExceptionType.ODATA_OPERATION_EXECUTION_FAILED)) {
				throw e;
			} else {
				result = retryExecuteWithCompleteUrl(null, providedClient, e);
			}
		}
		return result;
	}

	private ODataQueryResult retryExecuteWithCompleteUrl(String destinationName, HttpClient providedClient,
			ODataException e) throws ODataException {
		ODataQueryResult result;
		try {
			getUri(getServiceUrl(destinationName), getMetadataQuery(destinationName), null).toString();
		} catch (URISyntaxException e1) {
			throw new ODataException(ODataExceptionType.ODATA_OPERATION_EXECUTION_FAILED, REQUESTFAILURE, e);
		}
		this.isCacheRefresh = true;		
		result = internalExecute(destinationName, isMediaRequest, false, providedClient);
		return result;
	}

	ODataQueryResult execute(final String destinationName, boolean isMediaRequest) throws ODataException {
		ODataQueryResult result = null;
		this.isMediaRequest = isMediaRequest;
		try {
			result = internalExecute(destinationName, isMediaRequest, false, null);
		} catch (ODataException e) {
			if (e.getODataExceptionType().equals(ODataExceptionType.OTHER)
					|| e.getODataExceptionType().equals(ODataExceptionType.ODATA_OPERATION_EXECUTION_FAILED)) {
				throw e;
			} else {
				result = retryExecuteWithCompleteUrl(destinationName, null, e);
			}
		}
		return result;
	}

	/**
	 * Executes the OData query represented by this ODataQuery object.
	 * 
	 * @param withDestinationName
	 * @return ODataQueryResult represents the result of the query operation.
	 * @throws ODataException
	 */
	public ODataQueryResult execute(final WithDestinationName withDestinationName) throws ODataException {

		ODataQueryResult result = null;
		try {
			result = execute(withDestinationName.getDestinationName());
		} catch (ODataException e) {
			if (e.getODataExceptionType().equals(ODataExceptionType.OTHER)
					|| e.getODataExceptionType().equals(ODataExceptionType.ODATA_OPERATION_EXECUTION_FAILED)) {
				throw e;
			} else {
				this.isCacheRefresh = true;
				result = execute(withDestinationName.getDestinationName());
			}
		}
		return result;
	}

	/**
	 * Executes the OData query represented by this ODataQuery object.
	 * 
	 * @param destinationName
	 * @return ODataQueryResult represents the result of the query operation.
	 * @throws ODataException
	 */
	public ODataQueryResult execute(final String destinationName) throws ODataException {
		if (destinationName == null) {
			throw new ODataException(null,
					"Missing destination name configuration, " + "please declare an endpoint for the OData query.",
					null);
		}
		return execute(destinationName, false);
	}
	
	private ODataQueryResult internalExecute(final String destinationName, boolean isMediaRequest,
			boolean withDefaultClient, HttpClient providedClient) throws ODataException {
		// TODO This will fail when there is no Destination and there is a listener??
		// Need to check
		ODataQueryResult result;
		notifyQueryListeners(destinationName);
		try {
			if (StringUtils.isEmpty(servicePath)) {
				throw new ODataException(null, "Missing service in OData query.", null);
			}
			if (StringUtils.isEmpty(entity)) {
				throw new ODataException(null, "Missing entity in OData query.", null);
			}
			HttpClient httpClient = (destinationName == null ? providedClient
					: HttpClientAccessor.getHttpClient(destinationName));
			final URI serviceUrl = getServiceUrl(destinationName);

			httpResponse = loadEntriesFromDestination(destinationName, serviceUrl, isMediaRequest, httpClient);
			if(lastEntityMetadata != null){
				result = new ODataQueryResult(lastEntityMetadata, httpResponse, isMediaRequest);
			}else{
				result = new ODataQueryResult(entitymetadata, httpResponse, isMediaRequest);
			}
			result.setQuery(this);
			return result;
		} catch (final DestinationAccessException | URISyntaxException | IllegalStateException | IOException
				| DestinationNotFoundException | HttpClientInstantiationException | EdmException e) {
			if (e instanceof DestinationAccessException) {
				logger.error("Could not connect to destination service [No Access] :" + e.getMessage());
				logger.error("Could not connect to destination service [No Access] : " + e.getStackTrace());
			} else if (e instanceof DestinationNotFoundException) {
				logger.error("Could not connect to destination service [Not Found] :" + e.getMessage());
				logger.error("Could not connect to destination service [Not Found] : " + e.getStackTrace());
			} else if (e instanceof HttpClientInstantiationException) {
				logger.error("Could not connect to destination service [Can't Create HttpClient] :" + e.getMessage());
				logger.error(
						"Could not connect to destination service [Can't Create HttpClient] : " + e.getStackTrace());
			}

			throw new ODataException(ODataExceptionType.ODATA_OPERATION_EXECUTION_FAILED, REQUESTFAILURE, e);
		}finally {
			if(!isMediaRequest)
				HttpClientUtils.closeQuietly(httpResponse);
		}
	}

	private URI getServiceUrl(final String destinationName) throws URISyntaxException {
		URI servUri = (destinationName != null)
				? getUri(DestinationAccessor.getDestination(destinationName).getUri(),
						withSeparator(SEPARATOR_PATH, this.servicePath), null)
				: new URI(this.servicePath);
		return servUri;
	}

	private void notifyQueryListeners(final String destinationName) {
		for (ODataQueryListener ql : queryListeners) {
			// wrap in generic try/catch to make the ODataQuery resilient
			// against Exceptions from listeners
			try {
				if (destinationName != null) {
					ql.onQuery(destinationName, this.servicePath, this.entity);
				} else {
					ql.onQuery(new URI(this.servicePath), this.entity);
				}

			} catch (Exception e) {
				logger.error("Failure while invoking query listener of type " + ql.getClass(), e);
			}
		}
	}

	private HttpResponse loadEntriesFromDestination(String destinationName, final URI serviceUrl,
			boolean isMediaRequest, HttpClient httpClient) throws URISyntaxException, ODataException,
			DestinationNotFoundException, DestinationAccessException, HttpClientInstantiationException, EdmException {
		if (useMetadata && entitymetadata == null) {
			final URI uri = getUri(serviceUrl, withSeparator(SEPARATOR_PATH, METADATA), null);
			logRequestMeta(uri);
			try {
				loadMetadata(httpClient, uri, errorHandler);
			} catch (ODataException e) {
				throw new ODataException(ODataExceptionType.METADATA_FETCH_FAILED,
						"Failed to execute OData Metadata request.", e.getHttpStatusCode(),e);
			}
		}

		Map typeMap = new HashMap<>();
		if (entitymetadata != null) { // Currently this will not be null if
										// there is keys or filter set in the
										// query.
			try {
				entityType = entitymetadata.getEntityType();
				for (String p : entityType.getPropertyNames()) {
					typeMap.put(p, entityType.getProperty(p).getType());
				}
				if (keys != null)
					entity += '(' + convertKeyValuesToString(keys, entityType) + ')';
			} catch (EdmException e1) {
				throw new ODataException(ODataExceptionType.METADATA_PARSING_FAILED,
						"Error while parsing the metadata.", null);
			}
		}
		if (isMediaRequest) {
			entity = entity + "/$value";
		} else {
			requestQuery = oDataQueryResolver.getQuery(entityType);
		}
		String withSeperator = withSeparator(SEPARATOR_PATH, entity, requestLocalPath); 
		withSeperator = addNavigations(withSeperator);
		URI uri = getUri(serviceUrl, withSeperator, requestQuery);
		uri = uri.resolve(getUrlPath(serviceUrl, withSeperator, requestQuery));
		logRequest(uri, destinationName); // DestinationName passed for this
											// logging

		return new ODataRequestExecutor(httpClient, uri).errorHandler(errorHandler).headers(headers).execute();
	}

	public List getNavigations() {
		return navigationProperties;
	}

	public String addNavigations(String url) throws EdmException, ODataException {
		if (navigationProperties != null && !navigationProperties.isEmpty()) {
			StringBuilder urlBuilder = new StringBuilder();
			urlBuilder.append(url);
			for (ODataNavigationRequest navigationProperty : navigationProperties) {
				urlBuilder.append("/");
				urlBuilder.append(navigationProperty.getNavigationPropertyName());
				if (navigationProperty.getKeys() != null && navigationProperty.getNavigationType() != null) {
					urlBuilder.append('(');
					urlBuilder.append(convertKeyValuesToString(navigationProperty.getKeys(),
							navigationProperty.getNavigationType()));
					urlBuilder.append(')');
				}
			}
			return urlBuilder.toString();
		}
		return url;
	}
	
	public EdmEntitySet updateNavigationType(Edm edm) throws EdmException, ODataException{
		entitymetadata = edm.getDefaultEntityContainer().getEntitySet(getEntityName(entity));
		if(navigationProperties != null && !navigationProperties.isEmpty()){
		EdmEntityType navigationType = entitymetadata.getEntityType();
			for(ODataNavigationRequest property:navigationProperties){
				navigationType = updateNavigationTypeForProperty(property, navigationType, edm);
				property.setNavigationType(navigationType);
			}
			lastEntityMetadata = fetchEntityDetails(edm, navigationType);
		}
		return lastEntityMetadata;
	}
	
	private EdmEntitySet fetchEntityDetails(Edm edm, EdmEntityType navigationType) throws EdmException {
		for(EdmEntitySet entitySet : edm.getEntitySets()){
			if(navigationType != null && entitySet.getEntityType().getName().equals(navigationType.getName()) 
					&& entitySet.getEntityType().getNamespace().equals(navigationType.getNamespace())){
				return entitySet;
			}
		}
		return entitymetadata;
	}
	
	private EdmEntityType updateNavigationTypeForProperty(ODataNavigationRequest navigationProperty, EdmEntityType entityType, Edm edm) throws ODataException{
		EdmNavigationProperty navigation = null;
		try {
			navigation = (EdmNavigationProperty) entityType.getProperty(navigationProperty.getNavigationPropertyName());
			if (navigation != null) {
				EdmEntityType navigationType = navigation.getRelationship().getEnd1().getEntityType();
				if (navigationType.getName().equals(entityType.getName())) {
					return navigation.getRelationship().getEnd2().getEntityType();
				} else {
					return navigationType;
				}
			}
		} catch (EdmException e) {
			throw new ODataException(ODataExceptionType.METADATA_PARSING_FAILED,
					"Failed to load entity type for \"" + entity + "\"." + "navigation: "+ navigation, e);
		}
		
		return null;
	}
	
	private String getUrlPath(URI dest, String additionalPath, String query) {

		String adjustedPath = withSeparator(SEPARATOR_PATH, dest.getPath(), additionalPath);
		String adjustedQuery = withSeparatorOmitFirst(SEPARATOR_QUERY, dest.getQuery(), query);
		if (adjustedQuery.isEmpty()){
			adjustedQuery = null;
		}else{
			adjustedPath = adjustedPath + "?"+adjustedQuery;
		}
		return adjustedPath.replace(" ", "%20");
	}

	private void loadMetadata(HttpClient httpClient, URI uri, ErrorResultHandler errorResultHandler)
			throws ODataException {
		Edm edm = null;
		// **** needs to be refactored
		if (!cacheMetadata && metadataFliePath != null && !metadataFliePath.toString().isEmpty()) {
			try (InputStream is = metadataFliePath.openStream()) {
				edm = ODataClient.newInstance().readMetadata(is, true).getEdm();
				if (logger.isDebugEnabled()) {
					logger.debug(String.format("Fetched metadata from the file %s", metadataFliePath.toString()));
				}
			} catch (final EdmException e) {
				throw new ODataException(ODataExceptionType.METADATA_PARSING_FAILED,
						"Failed to read metadata for \"" + entity + "\".", e);
			} catch (IOException | EntityProviderException e) {
				logger.error("Failed to fetch the metadata", e);
				throw new ODataException(ODataExceptionType.METADATA_FETCH_FAILED,
						"Failed to fetch the metadata \"" + entity + "\".", e);
			} //*******

		} else {
			edm = metadataCache.getEdm(servicePath + "/$metadata", httpClient, destinationRelevantHeaders, errorResultHandler,
					cacheMetadata, metadataFliePath,cacheKey,isCacheRefresh);
		}
		try {
		
			updateNavigationType(edm);
		} catch (EdmException e) {
			throw new ODataException(ODataExceptionType.METADATA_PARSING_FAILED,
					"Failed to read metadata for \"" + entity + "\".", e);
		}
		if (entitymetadata == null) {
			throw new ODataException(ODataExceptionType.METADATA_PARSING_FAILED,
					"No entity found in metadata for \"" + entity + "\".");
		}

	}

	private String getEntityName(String entitywkeys) {
		String result = entitywkeys;
		if (entitywkeys.contains("(")) {
			result = entitywkeys.substring(0, entitywkeys.indexOf("("));
		}
		return result;
	}

	private URI getUri(final URI dest, final String additionalPath, final String query) throws URISyntaxException {

		final String adjustedPath = withSeparator(SEPARATOR_PATH, dest.getPath(), additionalPath);
		String adjustedQuery = withSeparatorOmitFirst(SEPARATOR_QUERY, dest.getQuery(), query);
		if (adjustedQuery.isEmpty())
			adjustedQuery = null;

		return new URI(dest.getScheme(), dest.getUserInfo(), dest.getHost(), dest.getPort(), adjustedPath,
				adjustedQuery, dest.getFragment());
	}

	private void logRequest(final URI uri, final String destinationName) {
		logger.debug("Executing OData entity query to \"" + servicePath + "\" with entity \"" + entity
				+ "\" and parameters \"" + StringUtils.defaultIfEmpty(requestQuery, "") + "\" on destination \""
				+ destinationName + "\". URI: " + uri + ".");
	}

	private void logRequestMeta(final URI uri) {
		logger.debug("Executing OData metadata query to " + uri + ".");
	}

	private static String withSeparatorOmitFirst(final String separator, final String... parts) {
		return StringUtils.removeStart(withSeparator(separator, parts), separator);
	}

	@Override
	public String toString() {
		String query = "";
		if (!isMediaRequest && !StringUtils.isEmpty(requestQuery)) {
			query = "?" + requestQuery;
		}
		if (!isMediaRequest && oDataQueryResolver != null) {
			try {
				query = "?" + oDataQueryResolver.getQuery(null);
			} catch (EdmException e) {
				return e.getMessage(); // This is sufficient since this method is used for testing and debugging
										// purposes only.
			}
		}
		return withSeparator(SEPARATOR_PATH, servicePath, entity, requestLocalPath) + query;
	}

	public String getMetadataQuery() {
		return forMetadata(servicePath);
	}

	private String getMetadataQuery(String destinationName) {
		if (destinationName == null) {
			try {
				return forMetadata(new URI(servicePath).getPath().toString());
			} catch (URISyntaxException e) {

				logger.error("Failure in metadata query population  " + e.getMessage());
				return null;
			}
		} else
			return forMetadata(servicePath);
	}

	public static String forMetadata(final String path) {
		return withSeparator(SEPARATOR_PATH, path, METADATA);
	}

  protected String getEntity() {
    return entity;
  }

  protected String getServicePath() {
    return servicePath;
  }

  protected Map getKeys() {
    return keys;
  }

  protected ODataQueryBuilder.ODataQueryResolver getoDataQueryResolver() {
    return oDataQueryResolver;
  }

  protected ErrorResultHandler getErrorHandler() {
    return errorHandler;
  }

  protected Map getHeaders() {
    return headers;
  }
	
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy