com.erudika.para.rest.ProxyResourceHandler Maven / Gradle / Ivy
/*
* Copyright 2013-2020 Erudika. http://erudika.com
*
* Licensed 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.
*
* For issues and patches go to: https://github.com/erudika
*/
package com.erudika.para.rest;
import com.erudika.para.DestroyListener;
import com.erudika.para.Para;
import com.erudika.para.core.App;
import com.erudika.para.core.ParaObject;
import com.erudika.para.core.utils.CoreUtils;
import com.erudika.para.core.utils.ParaObjectUtils;
import com.erudika.para.persistence.DAO;
import com.erudika.para.search.ElasticSearchUtils;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Pager;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.PATCH;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.HttpMethod.PUT;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.HttpHeaders;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Acts as a proxy for Elasticsearch and handles request to the custom resouce path {@code /v1/_elasticsearch}.
* @author Alex Bogdanovski [[email protected]]
*/
public class ProxyResourceHandler implements CustomResourceHandler {
private static final Logger logger = LoggerFactory.getLogger(ProxyResourceHandler.class);
private final String esScheme = Config.getConfigParam("es.restclient_scheme", Config.IN_PRODUCTION ? "https" : "http");
private final String esHost = Config.getConfigParam("es.restclient_host",
Config.getConfigParam("es.transportclient_host", "localhost"));
private final int esPort = Config.getConfigInt("es.restclient_port", 9200);
private RestClient lowLevelClient;
/**
* Resource path. Defaults to '_elasticsearch'.
*/
public static final String PATH = Config.getConfigParam("es.proxy_path", "_elasticsearch");
@Override
public String getRelativePath() {
return PATH;
}
@Override
public Response handleGet(ContainerRequestContext ctx) {
return proxyRequest(GET, ctx);
}
@Override
public Response handlePost(ContainerRequestContext ctx) {
return proxyRequest(POST, ctx);
}
@Override
public Response handlePatch(ContainerRequestContext ctx) {
return proxyRequest(PATCH, ctx);
}
@Override
public Response handlePut(ContainerRequestContext ctx) {
return proxyRequest(PUT, ctx);
}
@Override
public Response handleDelete(ContainerRequestContext ctx) {
return proxyRequest(DELETE, ctx);
}
Response proxyRequest(String method, ContainerRequestContext ctx) {
if (!Config.getConfigBoolean("es.proxy_enabled", false)) {
return Response.status(Response.Status.FORBIDDEN.getStatusCode(), "This feature is disabled.").build();
}
String appid = ParaObjectUtils.getAppidFromAuthHeader(ctx.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
if (StringUtils.isBlank(appid)) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
String path = getCleanPath(appid, getPath(ctx));
try {
if (path.endsWith("/reindex") && POST.equals(method)) {
return handleReindexTask(appid, ctx.getUriInfo().getQueryParameters().getFirst("destinationIndex"));
}
Header[] headers = getHeaders(ctx.getHeaders());
HttpEntity resp;
RestClient client = getClient();
if (client != null) {
if (ctx.getEntityStream() != null && ctx.getEntityStream().available() > 0) {
HttpEntity body = new InputStreamEntity(ctx.getEntityStream(), ContentType.APPLICATION_JSON);
resp = client.performRequest(method, path, Collections.emptyMap(), body, headers).getEntity();
} else {
resp = client.performRequest(method, path, headers).getEntity();
}
if (resp != null && resp.getContent() != null) {
Header type = resp.getContentType();
Object response = getTransformedResponse(appid, resp.getContent(), ctx);
return Response.ok(response).header(type.getName(), type.getValue()).build();
}
}
} catch (Exception ex) {
logger.warn("Failed to proxy '{} {}' to Elasticsearch: {}", method, path, ex.getMessage());
}
return Response.status(Response.Status.BAD_REQUEST).build();
}
private RestClient getClient() {
if (lowLevelClient != null) {
return lowLevelClient;
}
try {
lowLevelClient = RestClient.builder(new HttpHost(esHost, esPort, esScheme)).build();
Para.addDestroyListener(new DestroyListener() {
public void onDestroy() {
if (lowLevelClient != null) {
try {
lowLevelClient.close();
} catch (IOException ex) {
logger.error(null, ex);
}
}
}
});
} catch (Exception e) {
logger.error("Failed to initialize Elasticsearch low-level client: {}", e.getMessage());
}
return lowLevelClient;
}
private Header[] getHeaders(MultivaluedMap headers) {
if (headers == null || headers.isEmpty()) {
return new Header[0];
}
int i = 0;
headers.remove(CONTENT_LENGTH);
Header[] headerz = new Header[headers.size()];
for (String key : headers.keySet()) {
headerz[i] = new BasicHeader(key, headers.getFirst(key));
i++;
}
return headerz;
}
private String getPath(ContainerRequestContext ctx) {
String path = ctx.getUriInfo().getPathParameters(true).getFirst("path");
return StringUtils.isBlank(path) ? "_search" : path;
}
String getCleanPath(String appid, String path) {
if (StringUtils.containsIgnoreCase(path, "getRawResponse")) {
try {
URIBuilder uri = new URIBuilder(path);
List params = uri.getQueryParams();
for (Iterator iterator = params.iterator(); iterator.hasNext();) {
NameValuePair next = iterator.next();
if (next.getName().equalsIgnoreCase("getRawResponse")) {
iterator.remove();
break;
}
}
path = uri.setParameters(params).toString();
} catch (URISyntaxException ex) {
logger.warn(null, ex);
}
}
if (path.startsWith("/")) {
path = StringUtils.stripStart(path, "/");
}
if (StringUtils.isBlank(path) || "/".equals(path)) {
path = "_search";
}
// Prefix path with appid (alias) in order to route requests to the correct index for a particular app.
return "/".concat(appid).concat("/").concat(path);
}
private Response handleReindexTask(String appid, String destinationIndex) {
if (!Config.getConfigBoolean("es.proxy_reindexing_enabled", false) || appid == null) {
return Response.status(Response.Status.FORBIDDEN.getStatusCode(), "This feature is disabled.").build();
}
Pager pager = new Pager();
DAO dao = CoreUtils.getInstance().getDao();
App app = dao.read(App.id(appid));
if (app != null) {
long startTime = System.nanoTime();
ElasticSearchUtils.rebuildIndex(dao, app, destinationIndex, pager);
long tookMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
Map response = new HashMap();
response.put("reindexed", pager.getCount());
response.put("tookMillis", tookMillis);
return Response.ok(response, MediaType.APPLICATION_JSON).build();
} else {
return Response.status(404, "App not found.").build();
}
}
private Object getTransformedResponse(String appid, InputStream content, ContainerRequestContext ctx) {
if (ctx.getUriInfo().getQueryParameters().containsKey("getRawResponse") ||
StringUtils.containsIgnoreCase(getPath(ctx), "getRawResponse=")) {
return content;
} else {
try {
JsonNode tree = ParaObjectUtils.getJsonMapper().readTree(content);
JsonNode hits = tree.at("/hits/hits");
if (hits.isMissingNode()) {
return tree;
} else {
List keys = new LinkedList();
long count = tree.at("/hits/total").asLong();
for (JsonNode hit : hits) {
String id = hit.get("_id").asText();
keys.add(id);
}
DAO dao = CoreUtils.getInstance().getDao();
Map fromDB = dao.readAll(appid, keys, true);
Map result = new HashMap<>();
result.put("items", fromDB);
result.put("totalHits", count);
return result;
}
} catch (IOException ex) {
logger.error(null, ex);
}
return Collections.emptyMap();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy