controllers.oer.Application.java Source code

Java tutorial

Introduction

Here is the source code for controllers.oer.Application.java

Source

/* Copyright 2014 Fabian Steeg, hbz. Licensed under the Eclipse Public License 1.0 */

package controllers.oer;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.jena.riot.Lang;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.FilterBuilder;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.GeoPolygonFilterBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.mindrot.jbcrypt.BCrypt;

import play.Logger;
import play.api.http.MediaRange;
import play.libs.Json;
import play.mvc.BodyParser;
import play.mvc.Controller;
import play.mvc.Http.RawBuffer;
import play.mvc.Result;
import views.html.oer_index;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.jsonldjava.utils.JSONUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.io.BaseEncoding;

public class Application extends Controller {

    public static final String DATA_INDEX = "oer-data";
    static final String DATA_TYPE = "oer-type";
    public static final String USER_INDEX = "oer-users";
    private static final String USER_TYPE = "user-type";

    final static Client productionClient = new TransportClient(
            ImmutableSettings.settingsBuilder().put("cluster.name", "aither").build())
                    .addTransportAddress(new InetSocketTransportAddress("193.30.112.84", 9300));
    static Client client = productionClient;

    /**
     * @param newClient
     *            The new elasticsearch client to use.
     */
    public static void clientSet(Client newClient) {
        client = newClient;
    }

    /** Reset the elasticsearch client. */
    public static void clientReset() {
        client = productionClient;
    }

    @SuppressWarnings("javadoc")
    /* no javadoc for elements */
    public static enum Serialization {/* @formatter:off */
        JSON_LD(Lang.JSONLD, Arrays.asList("application/json", "application/ld+json")), RDF_XML(Lang.RDFXML,
                Arrays.asList("application/rdf+xml")), N_TRIPLE(Lang.NTRIPLES, Arrays.asList("text/plain")), N3(
                        Lang.N3, Arrays.asList("text/rdf+n3", "text/n3")), TURTLE(Lang.TURTLE,
                                Arrays.asList("application/x-turtle", "text/turtle"));/* @formatter:on */

        Lang format;
        List<String> types;

        /** @return The content types associated with this serialization. */
        public List<String> getTypes() {
            return types;
        }

        private Serialization(final Lang format, final List<String> types) {
            this.format = format;
            this.types = types;
        }
    }

    /**
     * Create a new user account. Pass two arguments: username and password.
     */
    public static void main(String... args) {
        if (args.length != 2) {
            System.err.println("Pass two arguments: username and password");
            System.exit(-1);
        }
        String user = args[0];
        String pass = BCrypt.hashpw(args[1], BCrypt.gensalt());
        System.out.print(responseInfo(client.prepareIndex(USER_INDEX, USER_TYPE, user)
                .setSource("user", user, "pass", pass).execute().actionGet()));
    }

    public static Result query(String q, String t, String location) {
        if (q.trim().isEmpty() && t.trim().isEmpty())
            return ok(oer_index.render(Arrays.asList(
                    // @formatter:off@
                    "/oer?q=\"Cape+Town\"", "/oer?q=*&t=http://schema.org/Organization",
                    "/oer?q=Africa&t=http://schema.org/Organization",
                    "/oer?q=\"http\\://www.oerafrica.org\""
                            + "&t=http://schema.org/Organization,http://schema.org/Service",
                    "/oer?q=*&location=40.8,-86.6+40.8,-88.6+42.8,-88.6+42.8,-86.6", "/oer?q=*&location=germany",
                    "/oer?q=\"Cape+Town\"&callback=callbackFunction", "/oer?q=University&from=0&size=5")));
        // @formatter:on@
        return processQuery(q, t, location);
    }

    public static Result get(String id) {
        try {
            String value = id.startsWith("http://") ? id : String.format("http://lobid.org/oer/%s#!", id);
            MatchQueryBuilder query = QueryBuilders.matchQuery("@graph.@id", value);
            SearchResponse response = search(DATA_INDEX, query, "", DATA_TYPE);
            boolean found = response.getHits().getTotalHits() > 0;
            return !found ? notFound()
                    : response(Json
                            .parse("[" + withoutLocation(response.getHits().getAt(0).getSourceAsString()) + "]"));
        } catch (Exception x) {
            x.printStackTrace();
            return internalServerError(x.getMessage());
        }
    }

    @BodyParser.Of(BodyParser.Raw.class)
    public static Result put(String id) throws UnsupportedEncodingException {
        RawBuffer rawBody = request().body().asRaw();
        if (rawBody == null)
            return badRequest("Expecting content in request body!\n");
        String contentType = request().getHeader(CONTENT_TYPE);
        if (contentType == null || contentType.isEmpty())
            return badRequest("Content-Type header required!");
        String authHeader = request().getHeader(AUTHORIZATION);
        if (authHeader == null || authHeader.isEmpty())
            return badRequest("Authorization required to write data!\n");
        if (!authorized(authHeader))
            return unauthorized("Not authorized to write data!\n");
        String requestBody = new String(rawBody.asBytes(), Charsets.UTF_8);
        return processRequest(id, authHeader, requestBody, contentType);
    }

    public static Result delete(String id) {
        String authHeader = request().getHeader(AUTHORIZATION);
        if (authHeader == null || authHeader.isEmpty())
            return badRequest("Authorization required to delete data!\n");
        if (!authorized(authHeader))
            return unauthorized("Not authorized to delete data!\n");
        try {
            DeleteResponse response = client.prepareDelete(DATA_INDEX, DATA_TYPE, id).execute().actionGet();
            return !response.isFound() ? notFound() : ok("Deleted " + id);
        } catch (Exception x) {
            x.printStackTrace();
            return internalServerError(x.getMessage());
        }
    }

    private static Result processRequest(String id, String auth, String requestBody, String contentType) {
        for (Serialization serialization : Serialization.values())
            for (String mimeType : serialization.getTypes())
                if (mimeType.equalsIgnoreCase(contentType)) {
                    Logger.info("Incoming Content-Type '{}' supported as format '{}'", mimeType,
                            serialization.format.getLabel());
                    return executeRequest(id, auth, serialization, requestBody);
                }
        return status(UNSUPPORTED_MEDIA_TYPE);
    }

    private static Result executeRequest(String id, String authHeader, Serialization serialization,
            String requestBody) {
        try {
            String jsonLd = NtToEs.rdfToJsonLd(requestBody, serialization.format);
            String parent = NtToEs.findParent(jsonLd);
            Logger.info("Storing under ID '{}' and parent '{}' data from user '{}': {}", id, parent,
                    userAndPass(authHeader)[0], jsonLd);
            return ok(responseInfo(client.prepareIndex(DATA_INDEX, DATA_TYPE, id).setSource(jsonLd)
                    .setParent(parent).execute().actionGet()));
        } catch (Exception e) {
            e.printStackTrace();
            String message = String.format("Could not process request body as format '%s': %s\n",
                    serialization.format.getLabel(), e.getMessage());
            return internalServerError(message);
        }
    }

    private static boolean authorized(String authHeader) {
        String[] userAndPass = userAndPass(authHeader);
        SearchResponse search = search(USER_INDEX, QueryBuilders.idsQuery(USER_TYPE).ids(userAndPass[0]), "",
                USER_TYPE);
        return search.getHits().getTotalHits() == 1
                && BCrypt.checkpw(userAndPass[1], (String) search.getHits().getAt(0).getSource().get("pass"));
    }

    private static String[] userAndPass(String authHeader) {
        try {
            String header = authHeader.replace("Basic", "").trim();
            return new String(BaseEncoding.base64().decode(header), "UTF-8").split(":");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new String[] { "unauthorized", "" };
    }

    private static Result processQuery(String q, String t, String location) {
        List<String> hits = hits(q, t, location);
        if (location.isEmpty())
            hits.addAll(useQueryTermAsLocation(q, t));
        String jsonString = "[" + Joiner.on(",").join(hits) + "]";
        return response(Json.parse(jsonString));
    }

    private static List<String> useQueryTermAsLocation(String q, String t) {
        return hits("*", t, q);
    }

    private static List<String> hits(String q, String t, String location) {
        BoolQueryBuilder query = QueryBuilders.boolQuery().must(QueryBuilders.queryString(q).field("_all"));
        if (!t.trim().isEmpty())
            query = query.must(typeQuery(t));
        SearchResponse response = search(DATA_INDEX, query, location, DATA_TYPE);
        List<String> hits = new ArrayList<String>();
        for (SearchHit hit : response.getHits())
            hits.add(withoutLocation(hit.getSourceAsString()));
        return hits;
    }

    private static Status response(JsonNode json) {
        /* JSONP callback support for remote server calls with JavaScript: */
        final String[] callback = request() == null || request().queryString() == null ? null
                : request().queryString().get("callback");
        Pair<String, Lang> negotiatedContent = negotiateContent(json);
        final Status notAcceptable = status(406, "Not acceptable: unsupported content type requested\n");
        if (invalidAcceptHeader() || negotiatedContent == null)
            return notAcceptable;
        if (callback != null)
            return ok(String.format("%s(%s)", callback[0], negotiatedContent.getLeft()));
        if (negotiatedContent.getRight().equals(Lang.JSONLD))
            return ok(Json.parse(negotiatedContent.getLeft()));
        return ok(negotiatedContent.getLeft());
    }

    private static Pair<String, Lang> negotiateContent(JsonNode json) {
        for (MediaRange mediaRange : request().acceptedTypes())
            for (Serialization serialization : Serialization.values())
                for (String mimeType : serialization.getTypes())
                    if (mediaRange.accepts(mimeType)) {
                        if (serialization.format.equals(Lang.JSONLD))
                            return Pair.of(withRemoteContext(json.toString()), Lang.JSONLD);
                        Logger.debug("Matching mime {}, converting JSON to {}", mimeType, serialization.format);
                        return Pair.of(NtToEs.jsonLdToRdf(json, serialization.format), serialization.format);
                    }
        return null;
    }

    private static String withRemoteContext(String string) {
        try {
            // JSON-LD compact, always an object (resulting in a map)
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> maps = (List<Map<String, Object>>) JSONUtils.fromString(string);
            for (Map<String, Object> map : maps) {
                map.put("@context", "http://api.lobid.org/oer/data/context.json");
            }
            return JSONUtils.toString(maps);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return string;
    }

    private static boolean invalidAcceptHeader() {
        if (request() == null)
            return true;
        final String acceptHeader = request().getHeader("Accept");
        return (acceptHeader == null || acceptHeader.trim().isEmpty());
    }

    private static String withoutLocation(String sourceAsString) {
        try {
            // JSON-LD compact, always an object (resulting in a map)
            @SuppressWarnings("unchecked")
            Map<String, Object> json = (Map<String, Object>) JSONUtils.fromString(sourceAsString);
            json.remove("location");
            return JSONUtils.toString(json);
        } catch (JsonParseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sourceAsString;
    }

    private static BoolQueryBuilder typeQuery(String t) {
        final String[] types = t.split(",");
        BoolQueryBuilder query = QueryBuilders.boolQuery();
        for (String type : types)
            query = query.should(
                    QueryBuilders.matchQuery("@graph.@type", type).operator(MatchQueryBuilder.Operator.AND));
        return query;
    }

    private static SearchResponse search(String index, QueryBuilder queryBuilder, String location, String type) {
        SearchRequestBuilder requestBuilder = client.prepareSearch(index)
                .setSearchType(SearchType.DFS_QUERY_THEN_FETCH).setQuery(queryBuilder).setTypes(type);
        if (!location.trim().isEmpty())
            requestBuilder = requestBuilder.setPostFilter(locationFilter(location));
        Logger.debug("Request:\n" + requestBuilder);
        SearchResponse response = requestBuilder.setFrom(get("from", 0)).setSize(get("size", 50)).setExplain(false)
                .execute().actionGet();
        Logger.debug("Response:\n" + response);
        return response;
    }

    private static int get(String parameterName, int defaultValue) {
        final String[] params = request() == null || request().queryString() == null ? null
                : request().queryString().get(parameterName);
        if (params != null)
            try {
                return Integer.parseInt(params[0]);
            } catch (NumberFormatException e) {
                e.printStackTrace();
                return defaultValue;
            }
        return defaultValue;
    }

    private static FilterBuilder locationFilter(String location) {
        if (location.matches(".*\\d+.*"))
            return polygonFilter(location);
        else
            return FilterBuilders.hasParentFilter("geonames-type",
                    QueryBuilders.queryString(location).field("_all"));
    }

    private static FilterBuilder polygonFilter(String location) {
        GeoPolygonFilterBuilder filter = FilterBuilders.geoPolygonFilter("oer-type.location");
        String[] points = location.split(" ");
        for (String point : points) {
            String[] latLon = point.split(",");
            filter = filter.addPoint(Double.parseDouble(latLon[0].trim()), Double.parseDouble(latLon[1].trim()));
        }
        return filter;
    }

    private static String responseInfo(IndexResponse r) {
        return String.format("Indexed into index %s, type %s, id %s, version %s\n", r.getIndex(), r.getType(),
                r.getId(), r.getVersion());
    }
}