Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/* *********************************************************************
* This Original Work is copyright of 51 Degrees Mobile Experts Limited.
* Copyright 2023 51 Degrees Mobile Experts Limited, Davidson House,
* Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
*
* This Original Work is licensed under the European Union Public Licence
* (EUPL) v.1.2 and is subject to its terms as set out below.
*
* If a copy of the EUPL was not distributed with this file, You can obtain
* one at https://opensource.org/licenses/EUPL-1.2.
*
* The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
* amended by the European Commission) shall be deemed incompatible for
* the purposes of the Work and the provisions of the compatibility
* clause in Article 5 of the EUPL shall not apply.
*
* If using the Work as, or as part of, a network application, by
* including the attribution notice(s) required under Article 5 of the EUPL
* in the end user terms of the application under an appropriate heading,
* such notice(s) shall fulfill the requirements of that article.
* ********************************************************************* */
package fiftyone.pipeline.cloudrequestengine.flowelements;
import fiftyone.pipeline.cloudrequestengine.CloudRequestException;
import fiftyone.pipeline.cloudrequestengine.data.CloudRequestData;
import fiftyone.pipeline.core.Constants;
import fiftyone.pipeline.core.data.AccessiblePropertyMetaData;
import fiftyone.pipeline.core.data.EvidenceKeyFilter;
import fiftyone.pipeline.core.data.EvidenceKeyFilterWhitelist;
import fiftyone.pipeline.core.data.FlowData;
import fiftyone.pipeline.core.data.factories.ElementDataFactory;
import fiftyone.pipeline.core.exceptions.PropertyNotLoadedException;
import fiftyone.pipeline.engines.data.AspectPropertyMetaData;
import fiftyone.pipeline.engines.data.AspectPropertyMetaDataDefault;
import fiftyone.pipeline.engines.flowelements.AspectEngineBase;
import fiftyone.pipeline.engines.services.HttpClient;
import fiftyone.pipeline.exceptions.AggregateException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static fiftyone.pipeline.cloudrequestengine.Constants.Messages.*;
import static fiftyone.pipeline.util.StringManipulation.stringJoin;
/**
* Engine that makes requests to the 51Degrees cloud service.
* @see Specification
*/
public class CloudRequestEngineDefault
extends AspectEngineBase
implements CloudRequestEngine {
private HttpClient httpClient;
private String endPoint;
private String resourceKey;
private String licenseKey;
private String propertiesEndpoint;
private String evidenceKeysEndpoint;
private String cloudRequestOrigin;
private Integer timeoutMillis;
private List propertyMetaData;
private volatile Map publicProperties;
private final Object publicPropertiesLock = new Object();
private volatile EvidenceKeyFilter evidenceKeyFilter;
private final Object evidenceKeyFilterLock = new Object();
public CloudRequestEngineDefault(
Logger logger,
ElementDataFactory aspectDataFactory,
HttpClient httpClient,
String endPoint,
String resourceKey,
String propertiesEndpoint,
String evidenceKeysEndpoint,
int timeoutMillis) {
this(
logger,
aspectDataFactory,
httpClient,
endPoint,
resourceKey,
null,
propertiesEndpoint,
evidenceKeysEndpoint,
timeoutMillis,
null);
}
public CloudRequestEngineDefault(
Logger logger,
ElementDataFactory aspectDataFactory,
HttpClient httpClient,
String endPoint,
String resourceKey,
String propertiesEndpoint,
String evidenceKeysEndpoint,
int timeoutMillis,
String cloudRequestOrigin) {
this(
logger,
aspectDataFactory,
httpClient,
endPoint,
resourceKey,
null,
propertiesEndpoint,
evidenceKeysEndpoint,
timeoutMillis,
cloudRequestOrigin);
}
public CloudRequestEngineDefault(
Logger logger,
ElementDataFactory aspectDataFactory,
HttpClient httpClient,
String endPoint,
String resourceKey,
String licenseKey,
String propertiesEndpoint,
String evidenceKeysEndpoint,
int timeoutMillis,
String cloudRequestOrigin) {
super(logger, aspectDataFactory);
this.endPoint = endPoint;
this.resourceKey = resourceKey;
this.licenseKey = licenseKey;
this.propertiesEndpoint = propertiesEndpoint;
this.evidenceKeysEndpoint = evidenceKeysEndpoint;
this.httpClient = httpClient;
this.cloudRequestOrigin = cloudRequestOrigin;
if (timeoutMillis > 0) {
this.timeoutMillis = timeoutMillis;
} else {
this.timeoutMillis = null;
}
propertyMetaData = new ArrayList<>();
propertyMetaData.add(new AspectPropertyMetaDataDefault(
"json-response",
this,
"",
String.class,
new ArrayList(),
true));
propertyMetaData.add(new AspectPropertyMetaDataDefault(
"process-started",
this,
"",
Boolean.class,
new ArrayList(),
true));
}
@Override
public List getProperties() {
return propertyMetaData;
}
@Override
public String getDataSourceTier() {
return "cloud";
}
@Override
public String getElementDataKey() {
return "cloud-response";
}
@Override
public EvidenceKeyFilter getEvidenceKeyFilter() throws CloudRequestException, AggregateException, PropertyNotLoadedException {
if (evidenceKeyFilter == null) {
synchronized (evidenceKeyFilterLock) {
if (evidenceKeyFilter == null) {
getCloudEvidenceKeys();
}
}
}
return evidenceKeyFilter;
}
@Override
public Map
getPublicProperties() throws CloudRequestException, AggregateException, PropertyNotLoadedException {
if (publicProperties == null) {
synchronized (publicPropertiesLock) {
if (publicProperties == null) {
getCloudProperties();
}
}
}
return publicProperties;
}
@Override
protected void processEngine(FlowData data, CloudRequestData aspectData) throws IOException {
byte[] content = getContent(data);
HttpURLConnection connection = httpClient.connect(new URL(endPoint.trim()));
if (timeoutMillis != null) {
connection.setConnectTimeout(timeoutMillis);
connection.setReadTimeout(timeoutMillis);
}
((CloudRequestDataInternal)aspectData).setProcessStarted(true);
Map headers = new HashMap<>();
setCommonHeaders(headers);
headers.put("Content-Type", "application/x-www-form-urlencoded");
headers.put("Content-Length", Integer.toString(content.length));
String response = httpClient.postData(connection, headers, content);
((CloudRequestDataInternal)aspectData).setJsonResponse(response);
validateResponse(response, connection);
}
/**
* Generate the Content to send in the POST request. The evidence keys
* e.g. 'query.' and 'header.' have an order of precedence. These are
* added to the evidence in reverse order, if there is conflict then
* the queryData value is overwritten.
* 'query.' evidence should take precedence over all other evidence.
* If there are evidence keys other than 'query.' that conflict then
* this is unexpected so a warning will be logged.
* @param data the FlowData
* @return form content for a POST request
* @throws UnsupportedEncodingException
*/
private byte[] getContent(FlowData data) throws UnsupportedEncodingException {
Map formData = getFormData(data);
List formItems = new ArrayList<>();
formItems.add("resource=" + resourceKey);
if(licenseKey != null && licenseKey.isEmpty() == false){
formItems.add("license=" + licenseKey);
}
List formKeys = Arrays.asList(formData.keySet().toArray(new String[0]));
Collections.sort(formKeys, Collections.reverseOrder());
for (String key : formKeys) {
formItems.add(key + "=" + URLEncoder.encode(formData.get(key).toString(), "UTF-8"));
}
String string = stringJoin(formItems, "&");
return string.getBytes(StandardCharsets.UTF_8);
}
Map getFormData(FlowData flowData) {
Map evidence = flowData.getEvidence().asKeyMap();
Map formData = new HashMap<>();
// Add evidence in reverse alphabetical order, excluding special keys.
addFormData(formData, evidence, getSelectedEvidence(evidence, "other"));
// Add cookie evidence.
addFormData(formData, evidence, getSelectedEvidence(evidence, "cookie"));
// Add header evidence.
addFormData(formData, evidence, getSelectedEvidence(evidence, "header"));
// Add query evidence.
addFormData(formData, evidence, getSelectedEvidence(evidence, "query"));
return formData;
}
/**
* Add form data to the evidence.
* @param formData the destination map to add the data to
* @param allEvidence all evidence in the FlowData. This is used to
* report which evidence keys are conflicting
* @param evidence evidence to add to the form data
*/
private void addFormData(
Map formData,
Map allEvidence,
Map evidence) {
List evidenceKeys = Arrays.asList(evidence.keySet().toArray(new String[0]));
Collections.sort(evidenceKeys, Collections.reverseOrder());
for (String evidenceKey : evidenceKeys) {
// Get the key parts
String[] evidenceKeyParts = evidenceKey.split(Pattern.quote(Constants.EVIDENCE_SEPERATOR));
String prefix = evidenceKeyParts[0];
String suffix = evidenceKeyParts[1];
// Check and add the evidence to the query parameters.
if (formData.containsKey(suffix) == false) {
formData.put(suffix, evidence.get(evidenceKey));
}
else {
// If the queryParameter exists already.
// Get the conflicting pieces of evidence and then log a
// warning, if the evidence prefix is not query. Otherwise a
// warning is not needed as query evidence is expected
// to overwrite any existing evidence with the same suffix.
if (prefix.equals("query") == false) {
Map conflicts = new HashMap<>();
for (String key : allEvidence.keySet()) {
if (key.equals(evidenceKey) == false && key.contains(suffix)) {
conflicts.put(key, allEvidence.get(key));
}
}
StringBuilder conflictStr = new StringBuilder();
for (Map.Entry conflict : conflicts.entrySet()) {
if (conflictStr.length() > 0) {
conflictStr.append(", ");
}
conflictStr.append(String.format("%s:%s", conflict.getKey(), conflict.getValue()));
}
String warningMessage = String.format(
fiftyone.pipeline.cloudrequestengine.Constants.Messages.EvidenceConflict,
evidenceKey,
evidence.get(evidenceKey),
conflictStr.toString());
logger.warn(warningMessage);
}
// Overwrite the existing queryParameter value.
formData.put(suffix, evidence.get(evidenceKey));
}
}
}
/**
* Get evidence with specified prefix.
* @param evidence all evidence in the FlowData
* @param type required evidence key prefix
* @return evidence with the required key prefix
*/
Map getSelectedEvidence(Map evidence, String type) {
Map selectedEvidence = new HashMap<>();
if (type.equals("other")) {
for (Map.Entry entry : evidence.entrySet()) {
if (hasKeyPrefix(entry.getKey(), "query") == false &&
hasKeyPrefix(entry.getKey(), "header") == false &&
hasKeyPrefix(entry.getKey(), "cookie") == false ) {
selectedEvidence.put(entry.getKey(), entry.getValue());
}
}
}
else {
for (Map.Entry entry : evidence.entrySet()) {
if (hasKeyPrefix(entry.getKey(), type)) {
selectedEvidence.put(entry.getKey(), entry.getValue());
}
}
}
return selectedEvidence;
}
/**
* Check that the key of a KeyValuePair has the given prefix.
* @param itemKey key to check
* @param prefix the prefix to check for
* @return true if the key has the prefix
*/
private boolean hasKeyPrefix(String itemKey, String prefix) {
return itemKey.startsWith(prefix + ".");
}
@Override
protected void unmanagedResourcesCleanup() {
}
private void setCommonHeaders(Map headers) {
if(cloudRequestOrigin != null &&
cloudRequestOrigin.length() > 0) {
headers.put(fiftyone.pipeline.cloudrequestengine.Constants.OriginHeaderName, cloudRequestOrigin);
}
}
private void getCloudProperties() throws CloudRequestException, AggregateException, PropertyNotLoadedException {
try {
String jsonResult;
Map headers = new HashMap<>();
setCommonHeaders(headers);
HttpURLConnection connection = httpClient.connect(new URL(propertiesEndpoint.trim() + (resourceKey != null ? "?Resource=" + resourceKey : "")));
jsonResult = httpClient.getResponseString(connection, headers);
validateResponse(jsonResult, connection);
JSONObject jsonObj = null;
if (jsonResult.isEmpty() == false) {
jsonObj = new JSONObject(jsonResult);
}
if (jsonObj != null) {
AccessiblePropertyMetaData.LicencedProducts accessiblePropertyData =
new AccessiblePropertyMetaData.LicencedProducts(jsonObj.getJSONObject("Products"));
publicProperties = accessiblePropertyData.products;
} else {
throw new RuntimeException("Failed to retrieve available properties " +
"from cloud service at " + propertiesEndpoint + ".");
}
} catch (IOException e) {
throw new PropertyNotLoadedException(e);
}
}
private void getCloudEvidenceKeys() throws CloudRequestException, AggregateException, PropertyNotLoadedException {
try {
String jsonResult;
Map headers = new HashMap<>();
setCommonHeaders(headers);
HttpURLConnection connection = httpClient.connect(new URL(evidenceKeysEndpoint.trim()));
jsonResult = httpClient.getResponseString(connection, headers);
validateResponse(jsonResult, connection, false);
if (jsonResult != null && jsonResult.isEmpty() == false) {
JSONArray jsonArray = new JSONArray(jsonResult);
List keys = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
keys.add(jsonArray.get(i).toString());
}
evidenceKeyFilter = new EvidenceKeyFilterWhitelist(keys,
String.CASE_INSENSITIVE_ORDER);
}
} catch (IOException e) {
throw new PropertyNotLoadedException(e);
}
}
/**
* Validate the JSON response from the cloud service.
* @param jsonResult the JSON content that is returned from the cloud
* @param connection a HttpURLConnection
*/
private void validateResponse(String jsonResult, HttpURLConnection connection)
throws IOException, CloudRequestException, AggregateException {
validateResponse(jsonResult, connection, true);
}
/**
* Validate the JSON response from the cloud service.
* @param jsonResult the JSON content that is returned from the cloud
* @param connection connection used when making the request
* @param checkForErrorMessages Set to false if the response will
* never contain error message text.
*/
private void validateResponse(String jsonResult,
HttpURLConnection connection,
boolean checkForErrorMessages)
throws IOException, CloudRequestException, AggregateException {
int code = connection.getResponseCode();
boolean hasData = jsonResult != null && jsonResult.isEmpty() == false;
List messages = new ArrayList<>();
if (hasData && checkForErrorMessages) {
JSONObject jObj = new JSONObject(jsonResult);
boolean hasErrors = jObj.keySet().contains("errors");
hasData = hasErrors ?
jObj.keySet().size() > 1 :
jObj.keySet().size() > 0;
if (hasErrors) {
JSONArray errors = jObj.getJSONArray("errors");
messages.addAll(
errors.toList()
.stream()
.map(Object::toString)
.collect(Collectors.toList()));
}
}
// If there were no errors but there was also no other data
// in the response then add an explanation to the list of
// messages.
if (messages.size() == 0 && hasData == false) {
String message = String.format(
MessageNoDataInResponse,
this.endPoint);
messages.add(message);
}
// If there is no detailed error message, but we got a
// non-success status code, then add a message to the list
else if (messages.size() == 0 && code != 200)
{
String message = String.format(
MessageErrorCodeReturned,
this.endPoint,
code,
jsonResult);
messages.add(message);
}
Map> headers = null;
if (messages.size() > 0){
headers = connection.getHeaderFields();
}
final Map> finalHeaders = headers;
// If there are any errors returned from the cloud service
// then throw an exception
if (messages.size() > 1) {
throw new AggregateException(
ExceptionCloudErrorsMultiple,
messages.stream()
.map(m -> new CloudRequestException(m, code, finalHeaders))
.collect(Collectors.toList()));
}
else if (messages.size() == 1) {
String message = String.format(
ExceptionCloudError,
messages.get(0));
throw new CloudRequestException(message, code, finalHeaders);
}
}
}