/******************************************************************************** | |
* Copyright (c) 2015-2018 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.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.lang3.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.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 = "{\"query\":{\"simple_query_string\":{\"query\":\"%s\",\"default_operator\":\"or\",\"lenient\":\"true\",\"fields\":[\"name^2\",\"_all\"]}}}"; | |
/** | |
* 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 String 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 = host + "/" + sourceName.toLowerCase() + "/_search?fields=_type,_id,_index&size=50"; | |
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(); | |
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 "TestStep": | |
clazz = TestStep.class; | |
break; | |
case "Measurement": | |
clazz = Measurement.class; | |
break; | |
case "Test": | |
clazz = Test.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); | |
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); | |
} | |
} | |
} |