blob: 6a15f28079aee749b62f6467d78e86aef1e45a3d [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
package org.eclipse.mdm.api.odsadapter.search;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.mdm.api.base.model.Entity;
import org.eclipse.mdm.api.base.model.Measurement;
import org.eclipse.mdm.api.base.model.Test;
import org.eclipse.mdm.api.base.model.TestStep;
import org.eclipse.mdm.api.base.query.DataAccessException;
import org.eclipse.mdm.api.dflt.model.Pool;
import org.eclipse.mdm.api.dflt.model.Project;
import org.eclipse.mdm.api.odsadapter.lookup.EntityLoader;
import org.eclipse.mdm.api.odsadapter.lookup.config.EntityConfig.Key;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* This class handles the requests which are sent to the FreeTextSearch
*
* @author Christian Weyermann
*
*/
public class ODSFreeTextSearch {
/**
* This is the payload which needs to be added to the post to add a
*/
private static final String ES_POSTDATA = "{\"_source\": [\"source\", \"type\", \"id\"], \"query\":{\"simple_query_string\":{\"query\":\"%s\",\"default_operator\":\"or\",\"lenient\":\"true\"}}}";
/**
* mainly logs requests on INFO
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ODSFreeTextSearch.class);
/**
* Used to finally load the Entites
*/
private EntityLoader loader;
/**
* The URL is hard coded
*/
private URI url;
/**
* The client is created once and reused
*/
private HttpClient client;
/**
* This will start up the FreeText Search. No upfron querries are done. Thus
* this can be called as often as desired without any major performance loss
*
* @param entityLoader
* @param sourceName
* @throws DataAccessException
*/
public ODSFreeTextSearch(EntityLoader entityLoader, String sourceName, String host) throws DataAccessException {
this.loader = entityLoader;
url = URI.create(host).resolve(sourceName.toLowerCase()).resolve("/_search");
client = new HttpClient();
}
/**
* A search which is compatible to the Search as defined in the rest of the API.
*
* @param inputQuery
* @return never null, but maybe empty
*/
public Map<Class<? extends Entity>, List<Entity>> search(String inputQuery) {
Map<Class<? extends Entity>, List<Entity>> result = new HashMap<>();
Map<Class<? extends Entity>, List<String>> instances = searchIds(inputQuery);
instances.keySet().forEach(type -> convertIds2Entities(result, instances, type));
return result;
}
/**
* A search which is compatible to the Search as defined in the rest of the API.
*
* @param inputQuery
* @return never null, but maybe empty
*/
public Map<Class<? extends Entity>, List<String>> searchIds(String inputQuery) {
Map<Class<? extends Entity>, List<String>> instanceIds = new HashMap<>();
JsonElement root = queryElasticSearch(inputQuery);
if (root != null) {
JsonArray hits = root.getAsJsonObject().get("hits").getAsJsonObject().get("hits").getAsJsonArray();
hits.forEach(e -> put(e, instanceIds));
}
return instanceIds;
}
/**
* Converts all instances to entities
*
* @param convertedMap it will
* @param map
* @param type
*/
private void convertIds2Entities(Map<Class<? extends Entity>, List<Entity>> convertedMap,
Map<Class<? extends Entity>, List<String>> map, Class<? extends Entity> type) {
try {
List<Entity> list = new ArrayList<>();
list.addAll(loader.loadAll(new Key<>(type), map.get(type)));
convertedMap.put(type, list);
} catch (DataAccessException e) {
throw new IllegalStateException("Cannot load ids from ODS. This means no results are available", e);
}
}
/**
* Puts all the hits in elasticsearch
*
* @param hit a hit as given from ElasticSearch
* @param map the map of all ids for the class of the entity
*/
private void put(JsonElement hit, Map<Class<? extends Entity>, List<String>> map) {
JsonObject object = hit.getAsJsonObject().get("_source").getAsJsonObject();
String type = object.get("type").getAsString();
Class<? extends Entity> clazz = getClass4Type(type);
if (clazz != null) {
if (!map.containsKey(clazz)) {
List<String> list = new ArrayList<>();
map.put(clazz, list);
}
List<String> list = map.get(clazz);
list.add((String) object.get("id").getAsString());
}
}
/**
*
* @param type the type as given by elasticsearch
* @return the class of each element
*/
private Class<? extends Entity> getClass4Type(String type) {
Class<? extends Entity> clazz;
switch (type) {
case "Project":
clazz = Project.class;
break;
case "Pool":
clazz = Pool.class;
break;
case "Test":
clazz = Test.class;
break;
case "TestStep":
clazz = TestStep.class;
break;
case "Measurement":
clazz = Measurement.class;
break;
default:
clazz = null;
}
return clazz;
}
/**
* This method actually querries ElasticSearch.
*
* @param inputQuery
* @return
*/
private JsonElement queryElasticSearch(String inputQuery) {
PostMethod post = new PostMethod(url.toString());
String requestJson = buildRequestJson(inputQuery);
LOGGER.info("POST: " + url);
LOGGER.info("Asking: " + requestJson);
byte[] json = requestJson.getBytes();
post.setRequestEntity(new ByteArrayRequestEntity(json, "application/json"));
JsonElement result = execute(post);
LOGGER.info("Answered:" + result);
return result;
}
/**
* Actually builds the json
*
* @param inputQuery
* @return
*/
private String buildRequestJson(String inputQuery) {
String query = StringEscapeUtils.escapeJson(inputQuery);
return String.format(ES_POSTDATA, query);
}
/**
* Executes the HTTP method and expects a json in the return payload, which is
* then returned
*
* @param method
* @return
*/
private JsonElement execute(HttpMethod method) {
try {
int status = client.executeMethod(method);
if (status == 404) {
return null;
}
checkError(status);
return buildResponseJson(method);
} catch (IOException e) {
throw new IllegalStateException("Problems querying ElasticSearch.", e);
}
}
/**
* Reads out the http method and builds the JSON via GSON
*
* @param method
* @return
* @throws IOException
*/
private JsonElement buildResponseJson(HttpMethod method) throws IOException {
JsonElement res = null;
InputStream in = method.getResponseBodyAsStream();
try (InputStreamReader reader = new InputStreamReader(in)) {
res = new JsonParser().parse(reader);
}
return res;
}
/**
* If an error occured an appropriate exception is thrown.
*
* @param status
*/
private void checkError(int status) {
String text = String.format("ElasticSearch answered %d. ", status);
if (status / 100 != 2) {
throw new IllegalStateException(text);
}
}
}