com.thoughtworks.studios.journey.JourneyService.java Source code

Java tutorial

Introduction

Here is the source code for com.thoughtworks.studios.journey.JourneyService.java

Source

/**
 * This file is part of journey-neo4j-plugin. journey-neo4j-plugin is a neo4j server extension that provides out-of-box action path analysis features on top of the graph database.
 *
 * Copyright 2015 ThoughtWorks, Inc. and Pengchao Wang
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.thoughtworks.studios.journey;

import com.thoughtworks.studios.journey.cspmining.*;
import com.thoughtworks.studios.journey.importexport.DataImportExport;
import com.thoughtworks.studios.journey.importexport.Reporter;
import com.thoughtworks.studios.journey.jql.DataQuery;
import com.thoughtworks.studios.journey.jql.DataQueryResult;
import com.thoughtworks.studios.journey.jql.JourneyQuery;
import com.thoughtworks.studios.journey.jql.Stop;
import com.thoughtworks.studios.journey.models.*;
import com.thoughtworks.studios.journey.utils.BatchTransaction;
import com.thoughtworks.studios.journey.utils.GraphDbUtils;
import com.thoughtworks.studios.journey.utils.JSONUtils;
import org.apache.commons.lang.StringUtils;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.index.Index;
import org.neo4j.index.lucene.ValueContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.*;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.thoughtworks.studios.journey.utils.JSONUtils.*;
import static com.thoughtworks.studios.journey.utils.MapUtils.mapOf;

@Path("/journey")
public class JourneyService {

    private static final Logger logger = LoggerFactory.getLogger(JourneyService.class);

    private GraphDatabaseService graphDB;

    public JourneyService(@Context GraphDatabaseService graphDB) {
        this.graphDB = graphDB;
    }

    /**
     * API for testing plugin installed correctly
     *
     * @return 200 response
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/status")
    public Response status() {
        return Response.status(Response.Status.OK).entity("OK").build();
    }

    /**
     * API for setup a schema namespace (indexes)
     *
     * @param ns: namespace name
     * @return 200 response
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{ns}/setup_schema")
    public Response setupSchema(@PathParam("ns") String ns) {
        Lock writingLock = getWritingLock(ns);
        writingLock.lock();
        try {
            Application app = new Application(graphDB, ns);
            try (Transaction tx = graphDB.beginTx()) {
                app.setupSchema();
                tx.success();
            }
        } finally {
            writingLock.unlock();
        }
        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for exporting all data under a namespace
     *
     * @param ns: namespace name
     * @return 200 response if migrate finished
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{ns}/export")
    public Response export(@PathParam("ns") String ns) {
        final Application app = new Application(graphDB, ns);
        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output)));
                try (Transaction ignored = graphDB.beginTx()) {
                    new DataImportExport(app).export(writer);
                }
            }
        };
        return Response.ok(stream).build();
    }

    /**
     * API for importing data exported via export api. The importing does not setup all the indexes,
     * so user need run /reindex api after importing.
     *
     * @param ns:     namespace
     * @param stream: request body stream
     * @return 200 response if import success
     * @throws IOException
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{ns}/import")
    public Response imports(@PathParam("ns") String ns, InputStream stream) throws IOException {
        Lock writingLock = getWritingLock(ns);
        writingLock.lock();
        try {
            final Application app = new Application(graphDB, ns);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream));
            try (BatchTransaction tx = new BatchTransaction(graphDB, 1000)) {
                DataImportExport importer = new DataImportExport(app, new Reporter() {
                    @Override
                    public void report() {
                        tx.increment();
                    }
                });
                importer.importFrom(bufferedReader);
            }
        } finally {
            writingLock.unlock();
        }
        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for run migrations under the namespace
     *
     * @param ns namespace name
     * @return 200 response
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{ns}/migrate")
    public Response migrate(@PathParam("ns") String ns) {
        Lock writingLock = getWritingLock(ns);
        writingLock.lock();
        try {
            Application app = new Application(graphDB, ns);
            ArrayList<Long> ids = new ArrayList<>();
            try (Transaction ignored = graphDB.beginTx()) {
                ResourceIterator<Node> nodes = graphDB.findNodes(app.journeys().getLabel());
                while (nodes.hasNext()) {
                    Node node = nodes.next();
                    ids.add(node.getId());
                }
            }

            try (BatchTransaction tx = new BatchTransaction(graphDB, 5000)) {
                for (Long id : ids) {
                    Node journey = graphDB.getNodeById(id);
                    if (!journey.hasProperty(Journeys.PROP_LENGTH)) {
                        Integer length = journey.getDegree(RelTypes.BELONGS_TO, Direction.INCOMING);
                        journey.setProperty(Journeys.PROP_LENGTH, length);
                        Index<Node> index = GraphDbUtils.legacyIndex(graphDB, app.journeys().getLabel());
                        index.add(journey, Journeys.PROP_LENGTH, new ValueContext(length).indexNumeric());
                    }
                    tx.increment();
                }
            }
        } finally {
            writingLock.unlock();
        }
        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for destroying all data under a namespace
     *
     * @param ns namespace name
     * @return 200 response
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{ns}/destroy")
    public Response destroy(@PathParam("ns") String ns) {
        Lock writingLock = getWritingLock(ns);
        writingLock.lock();
        try {
            Application app = new Application(graphDB, ns);
            try (Transaction tx = graphDB.beginTx()) {
                app.tearDownSchema();
                tx.success();
            }

            try (Transaction tx = graphDB.beginTx()) {
                app.journeys().tearDownLegacyIndex();
                tx.success();
            }

            app.destroyData();
        } finally {
            writingLock.unlock();
        }

        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for reindex journeys.
     *
     * @param ns namespace under operation
     * @return 200 response
     */
    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{ns}/reindex")
    public Response reindex(@PathParam("ns") String ns) {
        Lock writingLock = getWritingLock(ns);
        writingLock.lock();
        try {
            Application app = new Application(graphDB, ns);
            try (Transaction tx = graphDB.beginTx()) {
                app.journeys().tearDownLegacyIndex();
                tx.success();
            }

            ArrayList<Long> ids = new ArrayList<>();
            try (Transaction tx = graphDB.beginTx()) {
                ResourceIterator<Node> journeys = graphDB.findNodes(app.journeys().getLabel());
                while (journeys.hasNext()) {
                    ids.add(journeys.next().getId());
                }
                tx.success();
            }

            try (BatchTransaction tx = new BatchTransaction(graphDB, 100)) {
                for (Long id : ids) {
                    Node journey = graphDB.getNodeById(id);
                    app.journeys().reindex(journey);
                    tx.increment();
                }
            }
        } finally {
            writingLock.unlock();
        }

        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for adding multiple events.
     * Post events via request body in json format. e.g.
     *   [{
     *      "action_label":"do x",
     *      "start_at":1451956588844,
     *      "digest":"907e7d49",
     *      "anonymous_id":"9f0d0311",
     *      "session_id":"9f0d0311",
     *      "user":"johndoe@example.com",
     *      "properties":{
     *          "prop-a":"foo",
     *          "prop-b":"bar"
     *      }
     *    }]
     * @param ns: namespace under operation
     * @param eventsJSON: request body, json format, array event
     * @return 201 response
     * @throws IOException
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/add_events")
    public Response addEvents(@PathParam("ns") String ns, String eventsJSON) throws IOException {

        Lock writingLock = getWritingLock(ns);
        writingLock.lock();
        try {
            Application app = new Application(graphDB, ns);

            List<Map> eventsAttrs = jsonToListMap(eventsJSON);
            try (Transaction tx = graphDB.beginTx()) {
                for (Map eventAttrs : eventsAttrs) {
                    //noinspection unchecked
                    app.events().add(eventAttrs);
                }
                tx.success();
            }
        } finally {
            writingLock.unlock();
        }
        return Response.status(Response.Status.CREATED).build();
    }

    /**
     * API for identify a user and setup traits.
     * @param ns: namespace under operation
     * @param uid: unique identify for the user
     * @param anonymousId: anonymous_id to associate existing anonymous events
     * @param traitsJSON: request body for traits for the user
     * @return 201 response
     * @throws IOException
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/identify")
    public Response identify(@PathParam("ns") String ns, @QueryParam("uid") String uid,
            @QueryParam("anonymous_id") String anonymousId, String traitsJSON) throws IOException {
        Lock writingLock = getWritingLock(ns);
        writingLock.lock();

        try {
            Application app = new Application(graphDB, ns);
            Users users = app.users();

            try (Transaction tx = graphDB.beginTx()) {
                Node user = users.identify(uid, anonymousId);
                if (traitsJSON != null) {
                    Map<String, Object> traits = jsonToMap(traitsJSON);
                    for (String key : traits.keySet()) {
                        users.addTrait(user, key, traits.get(key));
                    }
                }
                tx.success();
            }
        } finally {
            writingLock.unlock();
        }
        return Response.status(Response.Status.CREATED).build();
    }

    private static ConcurrentHashMap<String, Lock> writingLocks = new ConcurrentHashMap<>();

    private static Lock getWritingLock(String ns) {
        Lock lock = new ReentrantLock();
        Lock existingLock = writingLocks.putIfAbsent(ns, lock);
        if (existingLock != null) {
            lock = existingLock;
        }
        return lock;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/user/{identifier}")
    public Response user(@PathParam("ns") String ns, @PathParam("identifier") String identifier)
            throws IOException {
        Application app = new Application(graphDB, ns);
        try (Transaction ignored = graphDB.beginTx()) {
            Node user = app.users().findByIdentifier(identifier);
            Map<String, Object> map = app.users().toHash(user);
            ArrayList<Map<String, Object>> journeys = new ArrayList<>();
            for (Node journey : app.users().journeys(user)) {
                journeys.add(app.journeys().toHash(journey));
            }
            map.put("journeys", journeys);
            return jsonOkResponse(map);
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/anonymous_user/{anonymous_id}")
    public Response anonymousUser(@PathParam("ns") String ns, @PathParam("anonymous_id") String anonymousId)
            throws IOException {
        Application app = new Application(graphDB, ns);
        try (Transaction ignored = graphDB.beginTx()) {
            Node user = app.users().findByAnonymousId(anonymousId);
            Map<String, Object> map = app.users().toHash(user);
            ArrayList<Map<String, Object>> journeys = new ArrayList<>();
            for (Node journey : app.users().journeys(user)) {
                journeys.add(app.journeys().toHash(journey));
            }
            map.put("journeys", journeys);
            return jsonOkResponse(map);
        }
    }

    /**
     * API for query journeys
     * @param ns: namespace under operation
     * @param queryJson: journey conditions. json format.
     * @param limit: limit of return result
     * @param offset: offset the result from start from
     * @param descOrder: whether in desc order
     * @param eventsLimit: max events loaded per journey
     * @return array of journeys in JSON format
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/journeys")
    public Response journeys(@PathParam("ns") String ns, @QueryParam("query") @DefaultValue("") String queryJson,
            @QueryParam("limit") @DefaultValue("100") int limit,
            @QueryParam("offset") @DefaultValue("0") int offset,
            @QueryParam("desc") @DefaultValue("true") boolean descOrder,
            @QueryParam("events_limit") @DefaultValue("50") int eventsLimit) throws IOException {
        Application app = new Application(graphDB, ns);
        List<String> conditions = parseQueryCondition(queryJson);
        List<Map> result = new ArrayList<>();

        try (Transaction ignored = graphDB.beginTx()) {
            JourneyQuery query = JourneyQuery.Builder.query(app).conditions(conditions).desc(descOrder).limit(limit)
                    .offset(offset).build();

            for (Node journey : query.journeys()) {
                result.add(app.journeys().toHash(journey, eventsLimit, 0));
            }
        }
        return jsonOkResponse(result);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/journeys_summary")
    public Response journeysSummary(@PathParam("ns") String ns,
            @QueryParam("query") @DefaultValue("") String queryJson) throws IOException {
        Application app = new Application(graphDB, ns);
        List<String> conditions = parseQueryCondition(queryJson);
        int journeyCount = 0;
        Set<Node> users = new HashSet<>();

        try (Transaction ignored = graphDB.beginTx()) {
            JourneyQuery query = JourneyQuery.Builder.query(app).conditions(conditions).build();
            for (Node journey : query.journeys()) {
                journeyCount++;
                users.add(app.journeys().user(journey));
            }

        }
        return jsonOkResponse(mapOf("journey_count", journeyCount, "user_count", users.size()));
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/journeys_action_graph")
    public Response journeysActionGraph(@PathParam("ns") String ns,
            @QueryParam("query") @DefaultValue("") String queryJson,
            @QueryParam("steps") @DefaultValue("10") int steps) throws IOException {
        Application app = new Application(graphDB, ns);
        List<String> conditions = parseQueryCondition(queryJson);
        ActionsGraph graph = new ActionsGraph(app, steps);
        try (Transaction ignored = graphDB.beginTx()) {
            JourneyQuery query = JourneyQuery.Builder.query(app).conditions(conditions).build();
            for (Node journey : query.journeys()) {
                graph.add(app.journeys().events(journey));
            }
        }
        return jsonOkResponse(graph);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/forward_journey_graph")
    public Response forwardJourneyGraph(@PathParam("ns") String ns, @QueryParam("query") String queryJson,
            @QueryParam("label") String startActionLabel, @QueryParam("steps") @DefaultValue("4") int steps)
            throws IOException {
        Application app = new Application(graphDB, ns);
        ActionsGraph graph = new ActionsGraph(app, steps);
        try (Transaction ignored = graphDB.beginTx()) {
            JourneyQuery query = JourneyQuery.Builder.query(app).conditions(parseQueryCondition(queryJson)).desc()
                    .build();
            for (Node journey : query.uniqueJourneys()) {
                if ("*".equals(startActionLabel)) {
                    graph.add(app.journeys().events(journey));
                } else {
                    graph.add(app.journeys().suffixFor(journey, startActionLabel));
                }
            }
        }
        return jsonOkResponse(graph);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/backtrace_journey_graph")
    public Response backtraceJourneyGraph(@PathParam("ns") String ns, @QueryParam("query") String queryJson,
            @QueryParam("label") String startActionLabel, @QueryParam("steps") @DefaultValue("4") int steps)
            throws IOException {
        Application app = new Application(graphDB, ns);
        ActionsGraph graph = new ActionsGraph(app, steps);
        try (Transaction ignored = graphDB.beginTx()) {
            JourneyQuery query = JourneyQuery.Builder.query(app).conditions(parseQueryCondition(queryJson)).desc()
                    .build();
            for (Node journey : query.uniqueJourneys()) {
                graph.add(app.journeys().reversedPrefixFor(journey, startActionLabel));
            }
        }
        return jsonOkResponse(graph);
    }

    /**
     * API for general data queries.
     * @param ns: namespace under operation.
     * @param select: select expression. Reference the data query document.
     * @param stopsJson: stop expression. Reference the data query document.
     * @param cross: whether to connect journeys for same user.
     * @return json format array of array of array.
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/query")
    public Response query(@PathParam("ns") String ns, @QueryParam("select") String select,
            @QueryParam("stops") String stopsJson, @QueryParam("cross") @DefaultValue("true") boolean cross)
            throws IOException {
        Application app = new Application(graphDB, ns);
        try (Transaction ignored = graphDB.beginTx()) {
            try {
                DataQuery dataQuery = new DataQuery(app, cross);
                dataQuery.select(select);
                dataQuery.addStops(JSONUtils.<Map<String, Object>>jsonToListOfT(stopsJson, Map.class));
                return jsonOkResponse(dataQuery.execute());
            } catch (Exception e) {
                logger.warn("Unexpected query error happened.", e);
                return jsonOkResponse(DataQueryResult.error(e));
            }
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/experiment_data")
    public Response experimentData(@PathParam("ns") String ns, @QueryParam("trait_name") String groupTraitName,
            @QueryParam("base_stop") String baseStopExpression,
            @QueryParam("convert_stop") String convertStopExpression) throws IOException {

        Application app = new Application(graphDB, ns);

        try (Transaction ignored = graphDB.beginTx()) {
            Stop baseStop = Stop.build(app, JSONUtils.jsonToMap(baseStopExpression));
            Stop convertStop = Stop.build(app, JSONUtils.jsonToMap(convertStopExpression));

            JourneyQuery query = baseStop.journeyQuery();

            List<Object[]> data = new ArrayList<>();

            for (Node journey : query.uniqueJourneys()) {
                Node user = app.journeys().user(journey);
                Set<Object> userGroups = app.users().getTraitValue(user, groupTraitName);
                if (userGroups.size() != 1) {
                    continue;
                }

                Object userGroup = userGroups.iterator().next();
                EventIterator iterator = app.journeys().eventIterator(journey, true);

                Stop.MatchResult baseMatch = baseStop.match(iterator);
                if (!baseMatch.matched()) {
                    continue;
                }

                Stop.MatchResult convertMatch = convertStop.match(iterator);
                data.add(new Object[] { userGroup, convertMatch.matched() ? 1 : 0 });
            }

            return jsonOkResponse(data);
        }
    }

    /**
     * API to load journeys by ids.
     * @param ns: namespace under operation.
     * @param ids: journey ids, numbers separated by ','.
     * @param eventsLimit: max number of events to be loaded per journey.
     * @param eventsOffset: offset for events pagination.
     * @return array of journeys in JSON format.
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/journeys/{ids}")
    public Response journeyByIds(@PathParam("ns") String ns, @PathParam("ids") String ids,
            @QueryParam("events_limit") @DefaultValue("50") int eventsLimit,
            @QueryParam("events_offset") @DefaultValue("0") int eventsOffset) throws IOException {
        Application app = new Application(graphDB, ns);

        List<Map> result = new ArrayList<>();
        try (Transaction ignored = graphDB.beginTx()) {
            for (Node journey : app.journeys().findByIds(StringUtils.split(ids, ","))) {
                result.add(app.journeys().toHash(journey, eventsLimit, eventsOffset));
            }
        }
        return jsonOkResponse(result);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/frequent_paths")
    public Response frequentPaths(@PathParam("ns") String ns,
            @QueryParam("ab") @DefaultValue("true") boolean absoluteSupport,
            @QueryParam("threshold") @DefaultValue("0.1") float threshold,
            @QueryParam("tree_category") @DefaultValue("GLOBAL") String treeCategory) throws IOException {
        Application app = new Application(graphDB, ns);
        List<Pattern> patterns = new ArrayList<>();
        try (Transaction ignored = graphDB.beginTx()) {
            for (SuffixTree tree : SuffixTree.findByCategory(app, TreeCategory.valueOf(treeCategory))) {
                CSPMiner miner = new CSPMiner(tree, threshold, absoluteSupport);
                for (Pattern pattern : miner.suffixPatterns()) {
                    patterns.add(pattern);
                }
            }
        }
        return jsonOkResponse(patterns);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/churn_actions")
    public Response churnPOSTActions(@PathParam("ns") String ns,
            @QueryParam("min_churn") @DefaultValue("2") int minNumberOfRepeats,
            @QueryParam("threshold") @DefaultValue("0.1") float threshold) throws IOException {
        Application app = new Application(graphDB, ns);
        List<RepeatedAction> actions = new ArrayList<>();

        try (Transaction ignored = graphDB.beginTx()) {
            for (SuffixTree tree : SuffixTree.findByCategory(app, TreeCategory.GLOBAL)) {
                Node action = app.actions().findByActionLabel(tree.getTreeName());
                if (app.actions().getHttpMethod(action).equals("GET")) {
                    continue;
                }

                RepeatedAction repeatedAction = new RepeatedAction(tree.getTreeName(), tree.getJourneyCount());

                CSPMiner miner = new CSPMiner(tree, threshold, false);

                for (Pattern pattern : miner.suffixPatterns()) {
                    if (pattern.numberOfLeadActionRepeats() >= minNumberOfRepeats) {
                        repeatedAction.addPattern(pattern);
                    }
                }

                if (repeatedAction.averageRepeats() >= minNumberOfRepeats) {
                    actions.add(repeatedAction);
                }
            }
        }
        return jsonOkResponse(actions);
    }

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/build_suffix_trees")
    public Response buildSuffixTrees(@PathParam("ns") String ns,
            @QueryParam("tree_height_limit") @DefaultValue("8") int treeHeightLimit,
            @QueryParam("journeys_limit") @DefaultValue("10000") int journeyLimit) throws IOException {
        Application app = new Application(graphDB, ns);

        destroySuffixTreesWithCategory(app, TreeCategory.GLOBAL);
        JourneyQuery query = JourneyQuery.Builder.query(app).limit(journeyLimit).build();

        new SuffixTreeBuilder(app, query, TreeCategory.GLOBAL, treeHeightLimit).build();

        return Response.status(Response.Status.CREATED).build();
    }

    private void destroySuffixTreesWithCategory(Application app, TreeCategory category) {
        try (Transaction tx = graphDB.beginTx()) {
            for (SuffixTree tree : SuffixTree.findByCategory(app, category)) {
                tree.destroy();
            }
            tx.success();
        }
    }

    private List<String> parseQueryCondition(String conditionJSON) throws IOException {
        return conditionJSON.length() == 0 ? new ArrayList<String>() : jsonToListString(conditionJSON);
    }

    private Response jsonOkResponse(Object results) throws IOException {
        return okResponse(toJson(results));
    }

    private Response okResponse(Object results) throws IOException {
        return Response.status(Response.Status.OK).entity(results).build();
    }

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/destroy_suffix_trees")
    public Response destroySuffixTrees(@PathParam("ns") String ns,
            @QueryParam("tree_category") @DefaultValue("GLOBAL") String treeCategory) throws IOException {
        Application app = new Application(graphDB, ns);
        destroySuffixTreesWithCategory(app, TreeCategory.valueOf(treeCategory));
        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for ignore or recover a label.
     * @param ns: namespace under operation.
     * @param label: label of action to be ignored or recovered.
     * @param toggle: 'true' for ignore, 'false' for recover.
     * @return 200 response.
     * @throws IOException
     */
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/ignore_action")
    public Response ignoreAction(@PathParam("ns") String ns, @QueryParam("label") String label,
            @QueryParam("toggle") @DefaultValue("true") boolean toggle) throws IOException {
        Application app = new Application(graphDB, ns);

        try (Transaction tx = graphDB.beginTx()) {
            Node action = app.actions().findByActionLabel(label);
            if (toggle) {
                app.actions().ignore(action);
            } else {
                app.actions().unIgnore(action);
            }

            tx.success();
        }
        return Response.status(Response.Status.OK).build();
    }

    /**
     * API for getting action labels under namespace (ignored label are excluded)
     * @param ns: namespace under operation
     * @return json format of list of string label
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/action_labels")
    public Response actionLabels(@PathParam("ns") String ns) throws IOException {
        Application app = new Application(graphDB, ns);
        List<String> labels = new ArrayList<>();
        try (Transaction ignored = graphDB.beginTx()) {
            Iterable<Node> actions = app.actions().allExcludeIgnored();
            for (Node action : actions) {
                labels.add(app.actions().getActionLabel(action));
            }
        }

        Collections.sort(labels);
        return jsonOkResponse(labels);
    }

    /**
     * API for getting all ignored actions
     * @param ns: namespace under operation
     * @return list of action label in json format
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/ignored_action_labels")
    public Response ignoredActionLabels(@PathParam("ns") String ns) throws IOException {
        Application app = new Application(graphDB, ns);
        LinkedList<String> labels = new LinkedList<>();
        try (Transaction ignored = graphDB.beginTx()) {
            Iterable<Node> actions = app.actions().allIgnored();
            for (Node action : actions) {
                labels.addLast(app.actions().getActionLabel(action));
            }
        }
        return jsonOkResponse(labels);
    }

    /**
     * API for getting all custom property names under namespace
     * @param ns namespace under operation
     * @return json format of list of string label
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/custom_properties")
    public Response customProperties(@PathParam("ns") String ns) throws IOException {
        Application app = new Application(graphDB, ns);
        List<String> names = new ArrayList<>();
        try (Transaction ignored = graphDB.beginTx()) {
            Iterable<Node> properties = app.customProperties().all();

            for (Node property : properties) {
                names.add(app.customProperties().getName(property));
            }
        }

        Collections.sort(names);
        return jsonOkResponse(names);

    }

    /**
     * API for getting all user traits names under namespace
     * @param ns namespace under operation
     * @return json format of list of string
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/user_traits")
    public Response userTraits(@PathParam("ns") String ns) throws IOException {
        Application app = new Application(graphDB, ns);
        List<String> names = new ArrayList<>();
        UserTraits module = app.userTraits();
        try (Transaction ignored = graphDB.beginTx()) {
            Iterable<Node> traits = module.all();
            for (Node trait : traits) {
                names.add(module.getName(trait));
            }
        }

        Collections.sort(names);
        return jsonOkResponse(names);
    }

    /**
     * Get sample values for all traits. OK it is not really a sampling for the performance reason.
     * What we get are likely to be added values because of how neo4j store the relationships
     *
     * @param ns namespace under operation
     * @return json format of map of trait name and values
     * @throws IOException
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{ns}/traits_sample")
    public Response traitsSample(@PathParam("ns") String ns,
            @QueryParam("sample") @DefaultValue("500") int sampleLimit) throws IOException {
        Application app = new Application(graphDB, ns);
        Map<String, Set> sampleResult = new HashMap<>();

        UserTraits module = app.userTraits();
        try (Transaction ignored = graphDB.beginTx()) {
            Iterable<Node> traits = module.all();
            for (Node trait : traits) {
                sampleResult.put(module.getName(trait), module.sampleValues(trait, sampleLimit));
            }
        }

        return jsonOkResponse(sampleResult);
    }

}