org.jaqpot.core.service.resource.ModelResource.java Source code

Java tutorial

Introduction

Here is the source code for org.jaqpot.core.service.resource.ModelResource.java

Source

/*
 *
 * JAQPOT Quattro
 *
 * JAQPOT Quattro and the components shipped with it (web applications and beans)
 * are licensed by GPL v3 as specified hereafter. Additional components may ship
 * with some other licence as will be specified therein.
 *
 * Copyright (C) 2014-2015 KinkyDesign (Charalampos Chomenidis, Pantelis Sopasakis)
 *
 * 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/>.
 * 
 * Source code:
 * The source code of JAQPOT Quattro is available on github at:
 * https://github.com/KinkyDesign/JaqpotQuattro
 * All source files of JAQPOT Quattro that are stored on github are licensed
 * with the aforementioned licence. 
 */
package org.jaqpot.core.service.resource;

import com.wordnik.swagger.annotations.*;

import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.ejb.EJB;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;

import org.apache.commons.validator.routines.UrlValidator;
import org.jaqpot.core.data.DatasetHandler;
import org.jaqpot.core.data.ModelHandler;
import org.jaqpot.core.data.UserHandler;
import org.jaqpot.core.model.Model;
import org.jaqpot.core.model.Task;
import org.jaqpot.core.model.User;
import org.jaqpot.core.model.dto.dataset.Dataset;
import org.jaqpot.core.model.dto.dataset.FeatureInfo;
import org.jaqpot.core.model.facades.UserFacade;
import org.jaqpot.core.model.factory.ErrorReportFactory;
import org.jaqpot.core.service.annotations.Authorize;
import org.jaqpot.core.service.annotations.UnSecure;
import org.jaqpot.core.service.data.PredictionService;
import org.jaqpot.core.service.exceptions.parameter.ParameterInvalidURIException;
import org.jaqpot.core.service.exceptions.parameter.ParameterIsNullException;
import org.jaqpot.core.service.exceptions.QuotaExceededException;
import org.jaqpot.core.service.validator.ParameterValidator;

/**
 *
 * @author Pantelis Sopasakis
 * @author Charalampos Chomenidis
 *
 */
@Path("model")
@Api(value = "/model", description = "Models API")
@Authorize
public class ModelResource {

    private static final Logger LOG = Logger.getLogger(ModelResource.class.getName());

    private static final String DEFAULT_DATASET = "http://app.jaqpot.org:8080/jaqpot/services/dataset/corona";

    @Context
    UriInfo uriInfo;

    @EJB
    ModelHandler modelHandler;

    @EJB
    DatasetHandler datasetHandler;

    @EJB
    UserHandler userHandler;

    @EJB
    PredictionService predictionService;

    @Context
    SecurityContext securityContext;

    @Inject
    @UnSecure
    Client client;

    @Inject
    ParameterValidator parameterValidator;

    @GET
    @Produces({ MediaType.APPLICATION_JSON, "text/uri-list" })
    @ApiOperation(value = "Finds all Models", notes = "Finds all Models from Jaqpot Dataset. The response will list all models and will return either a URI list "
            + "of a list of JSON model objects. In the latter case, only the IDs, metadata, ontological classes "
            + "and reliability of the models will be returned. "
            + "Use the parameters start and max to get paginated results.", response = Model.class, responseContainer = "List")
    @ApiResponses(value = { @ApiResponse(code = 200, message = "Models found and are listed in the response body"),
            @ApiResponse(code = 204, message = "No content: The request succeeded, but there are no models "
                    + "matching your search criteria."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response listModels(@ApiParam(value = "Authorization token") @HeaderParam("subjectid") String subjectId,
            @ApiParam(value = "start", defaultValue = "0") @QueryParam("start") Integer start,
            @ApiParam(value = "max - the server imposes an upper limit of 500 on this "
                    + "parameter.", defaultValue = "20") @QueryParam("max") Integer max) {
        if (max == null || max > 500) {
            max = 500;
        }
        String creator = securityContext.getUserPrincipal().getName();
        return Response.ok(modelHandler.listMetaOfCreator(creator, start != null ? start : 0, max))
                .header("total", modelHandler.countAllOfCreator(creator)).build();
    }

    @GET
    @Path("/featured")
    @Produces({ MediaType.APPLICATION_JSON, "text/uri-list" })
    @ApiOperation(value = "Finds all Models", notes = "Finds featured Models from Jaqpot database. The response will list all models and will return either a URI list "
            + "of a list of JSON model objects. In the latter case, only the IDs, metadata, ontological classes "
            + "and reliability of the models will be returned. "
            + "Use the parameters start and max to get paginated results.", response = Model.class, responseContainer = "List")
    @ApiResponses(value = { @ApiResponse(code = 200, message = "Models found and are listed in the response body"),
            @ApiResponse(code = 204, message = "No content: The request succeeded, but there are no models "
                    + "matching your search criteria."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response listFeaturedModels(
            @ApiParam(value = "Authorization token") @HeaderParam("subjectid") String subjectId,
            @ApiParam(value = "start", defaultValue = "0") @QueryParam("start") Integer start,
            @ApiParam(value = "max - the server imposes an upper limit of 500 on this "
                    + "parameter.", defaultValue = "20") @QueryParam("max") Integer max) {
        if (max == null || max > 500) {
            max = 500;
        }
        return Response.ok(modelHandler.findFeatured(start != null ? start : 0, max))
                .header("total", modelHandler.countFeatured()).build();
    }

    @GET
    @Produces({ MediaType.APPLICATION_JSON, "text/uri-list" })
    @Path("/{id}")
    @ApiOperation(value = "Finds Model by Id", notes = "Finds specified Model", response = Model.class)
    @ApiResponses(value = { @ApiResponse(code = 200, message = "Model is found"),
            @ApiResponse(code = 401, message = "You are not authorized to access this model"),
            @ApiResponse(code = 403, message = "This request is forbidden (e.g., no authentication token is provided)"),
            @ApiResponse(code = 404, message = "This model was not found."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response getModel(@PathParam("id") String id,
            @ApiParam(value = "Clients need to authenticate in order to access models") @HeaderParam("subjectid") String subjectId) {
        Model model = modelHandler.findModel(id);
        if (model == null) {
            return Response.ok(ErrorReportFactory.notFoundError(uriInfo.getPath()))
                    .status(Response.Status.NOT_FOUND).build();
        }
        return Response.ok(model).build();
    }

    @GET
    @Produces(MediaType.APPLICATION_XML)
    @Path("/{id}/pmml")
    @ApiOperation(value = "Finds Model by Id", notes = "Finds specified Model", response = Model.class)
    @ApiResponses(value = { @ApiResponse(code = 200, message = "Model is found"),
            @ApiResponse(code = 401, message = "You are not authorized to access this model"),
            @ApiResponse(code = 403, message = "This request is forbidden (e.g., no authentication token is provided)"),
            @ApiResponse(code = 404, message = "This model was not found."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response getModelPmml(@PathParam("id") String id,
            @ApiParam(value = "Clients need to authenticate in order to access models") @HeaderParam("subjectid") String subjectId)
            throws Throwable {
        Model model = modelHandler.findModelPmml(id);
        if (model == null || model.getPmmlModel() == null) {
            throw new NotFoundException("The requested model was not found on the server.");
        }

        Object pmmlObj = model.getPmmlModel();
        if (pmmlObj == null || pmmlObj.toString().isEmpty()) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity("This model does not have a PMML representation.").build();
        }
        if (pmmlObj instanceof List) {
            List pmmlObjList = (List) pmmlObj;
            Object pmml = pmmlObjList.stream().findFirst()
                    .orElseThrow(() -> new NotFoundException("This model does not have a PMML representation."));
            return Response.ok(pmml.toString(), MediaType.APPLICATION_XML).build();
        } else {
            return Response.ok(pmmlObj.toString(), MediaType.APPLICATION_XML).build();
        }
    }

    @GET
    @Produces({ "text/uri-list" })
    @Path("/{id}/independent")
    @ApiOperation(value = "Lists the independent features of a Model", notes = "Lists the independent features of a Model. The result is available as a URI list.", response = String.class, responseContainer = "List")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Model is found and its independent features are listed in the response body."),
            @ApiResponse(code = 401, message = "You are not authorized to access this model"),
            @ApiResponse(code = 403, message = "This request is forbidden (e.g., no authentication token is provided)"),
            @ApiResponse(code = 404, message = "This model was not found."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response listModelIndependentFeatures(@PathParam("id") String id,
            @ApiParam(value = "Clients need to authenticate in order to access models") @HeaderParam("subjectid") String subjectId) {

        Model foundModel = modelHandler.findModelIndependentFeatures(id);
        if (foundModel == null) {
            throw new NotFoundException("The requested model was not found on the server.");
        }
        return Response.ok(foundModel.getIndependentFeatures()).build();
    }

    @GET
    @Produces({ "text/uri-list" })
    @Path("/{id}/dependent")
    @ApiOperation(value = "Lists the dependent features of a Model", notes = "Lists the dependent features of a Model identified by its ID. The result is available as a URI list.", response = String.class, responseContainer = "List")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Model is found and its independent features are listed in the response body."),
            @ApiResponse(code = 401, message = "You are not authorized to access this model"),
            @ApiResponse(code = 403, message = "This request is forbidden (e.g., no authentication token is provided)"),
            @ApiResponse(code = 404, message = "This model was not found."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response listModelDependentFeatures(@PathParam("id") String id,
            @ApiParam(value = "Clients need to authenticate in order to access models") @HeaderParam("subjectid") String subjectId) {
        Model foundModel = modelHandler.findModelIndependentFeatures(id);
        if (foundModel == null) {
            throw new NotFoundException("The requested model was not found on the server.");
        }
        return Response.ok(foundModel.getDependentFeatures()).build();
    }

    @GET
    @Produces({ "text/uri-list" })
    @Path("/{id}/predicted")
    @ApiOperation(value = "Lists the dependent features of a Model", notes = "Lists the predicted features of a Model identified by its ID. The result is available as a URI list.", response = String.class, responseContainer = "List")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Model is found and its independent features are listed in the response body."),
            @ApiResponse(code = 401, message = "You are not authorized to access this model"),
            @ApiResponse(code = 403, message = "This request is forbidden (e.g., no authentication token is provided)"),
            @ApiResponse(code = 404, message = "This model was not found."),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response listModelPredictedFeatures(@PathParam("id") String id,
            @ApiParam(value = "Clients need to authenticate in order to access models") @HeaderParam("subjectid") String subjectId) {

        Model foundModel = modelHandler.findModel(id);
        if (foundModel == null) {
            throw new NotFoundException("The requested model was not found on the server.");
        }
        List<String> predictedFeatures = new ArrayList<>();
        predictedFeatures.addAll(foundModel.getPredictedFeatures());
        if (foundModel.getLinkedModels() != null) {
            foundModel.getLinkedModels().stream().map(m -> m.split("model/")[1]).forEach(mid -> {
                Model linkedModel = modelHandler.findModel(mid);
                predictedFeatures.addAll(linkedModel.getPredictedFeatures());
            });
        }
        return Response.ok(predictedFeatures).build();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{id}/required")
    @ApiOperation(value = "Lists the required features of a Model", notes = "Lists the required features of a Model identified by its ID. The result is available as a URI list.", response = String.class, responseContainer = "List")
    public Response listModelRequiredFeatures(@PathParam("id") String id,
            @HeaderParam("subjectId") String subjectId) {
        Model model = modelHandler.find(id);
        if (model == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        List<String> requiredFeatures;
        String datasetURI;

        if (model.getTransformationModels() != null && !model.getTransformationModels().isEmpty()) {
            Model firstTransformation = client.target(model.getTransformationModels().get(0)).request()
                    .accept(MediaType.APPLICATION_JSON).header("subjectId", subjectId).get(Model.class);
            requiredFeatures = firstTransformation.getIndependentFeatures();
            datasetURI = firstTransformation.getDatasetUri();
        } else {
            requiredFeatures = model.getIndependentFeatures();
            datasetURI = model.getDatasetUri();
        }
        Set<FeatureInfo> featureSet;
        if (datasetURI != null) {
            featureSet = client.target(datasetURI.split("\\?")[0] + "/features").request()
                    .accept(MediaType.APPLICATION_JSON).header("subjectId", subjectId)
                    .get(new GenericType<Set<FeatureInfo>>() {
                    });
        } else {
            featureSet = new HashSet<>();
        }

        Set<String> requiredFeatureSet = new HashSet<>(requiredFeatures);
        List<FeatureInfo> selectedFeatures = featureSet.stream()
                .filter(f -> requiredFeatureSet.contains(f.getURI())).collect(Collectors.toList());
        return Response.status(Response.Status.OK).entity(selectedFeatures).build();
    }

    @POST
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("/{id}")
    @ApiOperation(value = "Creates Prediction", notes = "Creates Prediction", response = Task.class)
    @org.jaqpot.core.service.annotations.Task
    public Response makePrediction(
            // defaultValue = DEFAULT_DATASET
            @ApiParam(name = "dataset_uri", required = true) @FormParam("dataset_uri") String datasetURI,
            @FormParam("visible") Boolean visible, @PathParam("id") String id,
            @HeaderParam("subjectid") String subjectId) throws GeneralSecurityException, QuotaExceededException,
            ParameterIsNullException, ParameterInvalidURIException {

        if (datasetURI == null) {
            throw new ParameterIsNullException("datasetURI");
        }
        if (id == null) {
            throw new ParameterIsNullException("id");
        }

        UrlValidator urlValidator = new UrlValidator();
        if (!urlValidator.isValid(datasetURI)) {
            throw new ParameterInvalidURIException("Not valid dataset URI.");
        }

        User user = userHandler.find(securityContext.getUserPrincipal().getName());
        long datasetCount = datasetHandler.countAllOfCreator(user.getId());
        int maxAllowedDatasets = new UserFacade(user).getMaxDatasets();

        if (datasetCount > maxAllowedDatasets) {
            LOG.info(String.format("User %s has %d datasets while maximum is %d", user.getId(), datasetCount,
                    maxAllowedDatasets));
            throw new QuotaExceededException("Dear " + user.getId()
                    + ", your quota has been exceeded; you already have " + datasetCount + " datasets. "
                    + "No more than " + maxAllowedDatasets + " are allowed with your subscription.");
        }

        Model model = modelHandler.find(id);
        if (model == null) {
            throw new NotFoundException("Model not found.");
        }
        String datasetId = datasetURI.split("dataset/")[1];
        Dataset datasetMeta = datasetHandler.findMeta(datasetId);
        List<String> requiredFeatures = retrieveRequiredFeatures(model);

        parameterValidator.validateDataset(datasetMeta, requiredFeatures);

        Map<String, Object> options = new HashMap<>();
        options.put("dataset_uri", datasetURI);
        options.put("subjectid", subjectId);
        options.put("modelId", id);
        options.put("creator", securityContext.getUserPrincipal().getName());
        options.put("base_uri", uriInfo.getBaseUri().toString());
        Task task = predictionService.initiatePrediction(options);
        return Response.ok(task).build();
    }

    @DELETE
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON, "text/uri-list" })
    @ApiOperation(value = "Deletes a particular Model resource", notes = "Deletes a Model of a given ID. The method is idempondent, that is it can be used more than once without "
            + "triggering an exception/error. If the Model does not exist, the method will return without errors. "
            + "Authentication and authorization requirements apply, so clients that are not authenticated with a "
            + "valid token or do not have sufficient priviledges will not be able to delete Models using this method.")
    @ApiResponses(value = { @ApiResponse(code = 200, message = "Model entry was deleted successfully (if found)."),
            @ApiResponse(code = 401, message = "You are not authorized to delete this resource"),
            @ApiResponse(code = 403, message = "This request is forbidden (e.g., no authentication token is provided)"),
            @ApiResponse(code = 500, message = "Internal server error - this request cannot be served.") })
    public Response deleteModel(
            @ApiParam("Clients need to authenticate in order to create resources on the server") @HeaderParam("subjectid") String subjectId,
            @ApiParam(value = "ID of the Model.", required = true) @PathParam("id") String id) {
        Model model = modelHandler.find(id);
        if (model == null) {
            throw new NotFoundException("The model with id:" + id + " was not found.");
        }
        String userName = securityContext.getUserPrincipal().getName();
        if (!model.getMeta().getCreators().contains(userName)) {
            return Response.status(Response.Status.FORBIDDEN)
                    .entity("You cannot delete a Model that was not created by you.").build();
        }
        if (model.getTransformationModels() != null) {
            for (String transformationModel : model.getTransformationModels()) {
                modelHandler.remove(new Model(transformationModel));
            }
        }
        if (model.getLinkedModels() != null) {
            for (String linkedModel : model.getLinkedModels()) {
                modelHandler.remove(new Model(linkedModel));
            }
        }
        modelHandler.remove(new Model(id));
        return Response.ok().build();
    }

    private List<String> retrieveRequiredFeatures(Model model) {
        if (model.getTransformationModels() != null && !model.getTransformationModels().isEmpty()) {
            String transModelId = model.getTransformationModels().get(0).split("model/")[1];
            Model transformationModel = modelHandler.findModelIndependentFeatures(transModelId);
            if (transformationModel != null && transformationModel.getIndependentFeatures() != null) {
                return transformationModel.getIndependentFeatures();
            }
        }
        return model.getIndependentFeatures();
    }
}