/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package org.roda.wui.api.v1;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.lang3.StringUtils;
import org.roda.core.RodaCoreFactory;
import org.roda.core.common.UserUtility;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.RODAException;
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.utils.JsonUtils;
import org.roda.core.data.v2.index.CountRequest;
import org.roda.core.data.v2.index.FindRequest;
import org.roda.core.data.v2.index.IndexResult;
import org.roda.core.data.v2.index.IsIndexed;
import org.roda.core.data.v2.index.facet.FacetParameter;
import org.roda.core.data.v2.index.facet.FacetParameter.SORT;
import org.roda.core.data.v2.index.facet.Facets;
import org.roda.core.data.v2.index.facet.SimpleFacetParameter;
import org.roda.core.data.v2.index.filter.Filter;
import org.roda.core.data.v2.index.filter.SimpleFilterParameter;
import org.roda.core.data.v2.index.sort.SortParameter;
import org.roda.core.data.v2.index.sort.Sorter;
import org.roda.core.data.v2.index.sublist.Sublist;
import org.roda.core.data.v2.user.User;
import org.roda.core.index.utils.IterableIndexResult;
import org.roda.wui.api.controllers.Browser;
import org.roda.wui.api.v1.utils.ApiUtils;
import org.roda.wui.api.v1.utils.ExtraMediaType;
import org.roda.wui.api.v1.utils.FacetsCSVOutputStream;
import org.roda.wui.api.v1.utils.ResultsCSVOutputStream;
import org.roda.wui.common.I18nUtility;
import org.roda.wui.common.server.RodaStreamingOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
/**
* REST API resource Index.
*
* @author Rui Castro <rui.castro@gmail.com>
*/
@Path("/v1/index")
@Api(value = "v1 index")
public class IndexResource {
/**
* Logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(IndexResource.class);
/**
* Default value for <i>start</i> parameter.
*/
private static final int DEFAULT_START = 0;
/**
* Default value for <i>limit</i> parameter.
*/
private static final int DEFAULT_LIMIT = 100;
/**
* Default value for <i>onlyActive</i> parameter.
*/
private static final boolean DEFAULT_ONLY_ACTIVE = true;
/**
* Default filename for CSV files.
*/
private static final String DEFAULT_CSV_FILENAME = "export.csv";
/**
* CSV type.
*/
private static final String TYPE_CSV = "csv";
/**
* Default value for <i>facetLimit</i> parameter.
*/
private static final int DEFAULT_FACET_LIMIT = 100;
/** CSV field delimiter config key. */
private static final String CONFIG_KEY_CSV_DELIMITER = "csv.delimiter";
/**
* HTTP request.
*/
@Context
private HttpServletRequest request;
/**
* Find indexed resources.
*
* @param returnClass
* {@link Class} of resources to return.
* @param filterParameters
* List of filter parameters. Example: "formatPronom=fmt/19".
* @param sortParameters
* List of sort parameters. Examples: "formatPronom", "uuid desc".
* @param start
* Index of the first element to return (0-based index).
* @param limit
* Maximum number of elements to return.
* @param facetAttributes
* Facets to return.
* @param facetLimit
* Maximum number of facets to return.
* @param localeString
* the locale.
* @param onlyActive
* Return only active resources?
* @param exportFacets
* for CSV results, export only facets?
* @param filename
* the filename for exported CSV.
* @param <T>
* Type of the resources to return.
* @return a {@link Response} with the resources.
* @throws RODAException
* if some error occurs.
*/
@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, ExtraMediaType.TEXT_CSV})
@ApiOperation(value = "Find indexed resources", notes = "Find indexed resources.", response = IndexResult.class, responseContainer = "List")
public <T extends IsIndexed> Response list(
@ApiParam(value = "Class of resources to return", required = true, example = "org.roda.core.data.v2.ip.IndexedFile") @QueryParam(RodaConstants.API_QUERY_KEY_RETURN_CLASS) final String returnClass,
@ApiParam(value = "Filter parameters", example = "formatPronom=fmt/19") @QueryParam(RodaConstants.API_QUERY_KEY_FILTER) final List<String> filterParameters,
@ApiParam(value = "Sort parameters", example = "\"formatPronom\", \"uuid desc\"") @QueryParam(RodaConstants.API_QUERY_KEY_SORT) final List<String> sortParameters,
@ApiParam(value = "Index of the first element to return (0-based index)", defaultValue = "0") @QueryParam(RodaConstants.API_QUERY_KEY_START) final Integer start,
@ApiParam(value = "Maximum number of elements to return", defaultValue = "100") @QueryParam(RodaConstants.API_QUERY_KEY_LIMIT) final Integer limit,
@ApiParam(value = "Facets to return", example = "formatPronom") @QueryParam(RodaConstants.API_QUERY_KEY_FACET) final List<String> facetAttributes,
@ApiParam(value = "Facet limit", example = "100", defaultValue = "100") @QueryParam(RodaConstants.API_QUERY_KEY_FACET_LIMIT) final Integer facetLimit,
@ApiParam(value = "Language", example = "en", defaultValue = "en") @QueryParam(RodaConstants.API_QUERY_KEY_LANG) final String localeString,
@ApiParam(value = "Return only active resources?", defaultValue = "true") @QueryParam(RodaConstants.API_QUERY_KEY_ONLY_ACTIVE) final Boolean onlyActive,
@ApiParam(value = "Export facet data", defaultValue = "false") @QueryParam(RodaConstants.API_QUERY_KEY_EXPORT_FACETS) final boolean exportFacets,
@ApiParam(value = "Filename", defaultValue = DEFAULT_CSV_FILENAME) @QueryParam(RodaConstants.API_QUERY_KEY_FILENAME) final String filename)
throws RODAException {
final String mediaType = ApiUtils.getMediaType(null, request);
final User user = UserUtility.getApiUser(request);
final FindRequest findRequest = new FindRequest();
findRequest.classToReturn = returnClass;
findRequest.exportFacets = exportFacets;
findRequest.filename = StringUtils.isBlank(filename) ? DEFAULT_CSV_FILENAME : filename;
findRequest.filter = new Filter();
for (String filterParameter : filterParameters) {
final String[] parts = filterParameter.split("=");
if (parts.length == 2) {
findRequest.filter.add(new SimpleFilterParameter(parts[0], parts[1]));
} else {
LOGGER.warn("Unable to parse filter parameter '{}'. Ignored", filterParameter);
}
}
findRequest.sorter = new Sorter();
for (String sortParameter : sortParameters) {
final String[] parts = sortParameter.split(" ");
final boolean descending = parts.length == 2 && "desc".equalsIgnoreCase(parts[1]);
if (parts.length > 0) {
findRequest.sorter.add(new SortParameter(parts[0], descending));
} else {
LOGGER.warn("Unable to parse sorter parameter '{}'. Ignored", sortParameter);
}
}
findRequest.sublist = new Sublist(start == null ? DEFAULT_START : start, limit == null ? DEFAULT_LIMIT : limit);
final int paramFacetLimit = facetLimit == null ? DEFAULT_FACET_LIMIT : facetLimit;
final Set<FacetParameter> facetParameters = new HashSet<>();
for (String facetAttribute : facetAttributes) {
facetParameters.add(new SimpleFacetParameter(facetAttribute, paramFacetLimit, SORT.COUNT));
}
findRequest.facets = new Facets(facetParameters);
findRequest.onlyActive = onlyActive == null ? DEFAULT_ONLY_ACTIVE : onlyActive;
final Response response;
if (ExtraMediaType.TEXT_CSV.equals(mediaType)) {
response = csvResponse(findRequest, user);
} else {
final Class<T> classToReturn = getClass(findRequest.classToReturn);
IndexResult<T> indexResult = Browser.find(classToReturn, findRequest.filter, findRequest.sorter,
findRequest.sublist, findRequest.facets, user, findRequest.onlyActive, new ArrayList<>());
indexResult = I18nUtility.translate(indexResult, classToReturn, localeString);
response = Response.ok(indexResult, mediaType).build();
}
return response;
}
/**
* Find indexed resources.
*
* @param findRequest
* find parameters.
* @param <T>
* Type of the resources to return.
* @return a {@link Response} with the resources.
* @throws RODAException
* if some error occurs.
*/
@POST
@Path("/find")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, ExtraMediaType.TEXT_CSV})
@ApiOperation(value = "Find indexed resources", notes = "Find indexed resources.", response = IsIndexed.class, responseContainer = "List")
public <T extends IsIndexed> Response find(@ApiParam(value = "Find parameters") final FindRequest findRequest)
throws RODAException {
final String mediaType = ApiUtils.getMediaType(null, request);
final User user = UserUtility.getApiUser(request);
if (ExtraMediaType.TEXT_CSV.equals(mediaType)) {
return csvResponse(findRequest, user);
} else {
final IndexResult<T> result = Browser.find(getClass(findRequest.classToReturn), findRequest.filter,
findRequest.sorter, findRequest.sublist, findRequest.facets, user, findRequest.onlyActive, new ArrayList<>());
return Response.ok(result, mediaType).build();
}
}
/**
* Find indexed resources.
*
* @param findRequestString
* find parameters.
* @param type
* the type of output ("csv").
* @return a {@link Response} with the resources.
* @throws RODAException
* if some error occurs.
*/
@POST
@Path("/findFORM")
@Consumes({MediaType.APPLICATION_FORM_URLENCODED})
public Response findFORM(@FormParam("findRequest") final String findRequestString,
@FormParam("type") final String type) throws RODAException {
final User user = UserUtility.getApiUser(request);
final FindRequest findRequest = JsonUtils.getObjectFromJson(findRequestString, FindRequest.class);
if (type.equals(IndexResource.TYPE_CSV)) {
return csvResponse(findRequest, user);
} else {
// TODO support JSON type
throw new GenericException("Type not yet supported:" + type);
}
}
/**
* Count indexed resources.
*
* @param countRequest
* count parameters.
* @return a {@link Response} with the count.
* @throws RODAException
* if some error occurs.
*/
@POST
@Path("/count")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@ApiOperation(value = "Count indexed resources", notes = "Count indexed resources.", response = Long.class)
public Response count(@ApiParam(value = "Count parameters") final CountRequest countRequest) throws RODAException {
final String mediaType = ApiUtils.getMediaType(null, request);
final User user = UserUtility.getApiUser(request);
final long result = Browser.count(user, getClass(countRequest.classToReturn), countRequest.filter,
countRequest.onlyActive);
return Response.ok(result, mediaType).build();
}
/**
* Produces a CSV response with results or facets.
*
* @param findRequest
* the request parameters.
* @param user
* the current {@link User}.
* @param <T>
* Type of the resources to return.
* @return a {@link Response} with CSV.
* @throws RequestNotValidException
* it the request is not valid.
* @throws AuthorizationDeniedException
* if the user is not authorized to perform this operation.
* @throws GenericException
* if some other error occurs.
*/
private <T extends IsIndexed> Response csvResponse(final FindRequest findRequest, final User user)
throws RequestNotValidException, AuthorizationDeniedException, GenericException {
final Class<T> returnClass = getClass(findRequest.classToReturn);
final Configuration config = RodaCoreFactory.getRodaConfiguration();
final char delimiter;
if (StringUtils.isBlank(config.getString(CONFIG_KEY_CSV_DELIMITER))) {
delimiter = CSVFormat.DEFAULT.getDelimiter();
} else {
delimiter = config.getString(CONFIG_KEY_CSV_DELIMITER).trim().charAt(0);
}
if (findRequest.exportFacets) {
final IndexResult<T> result = Browser.find(returnClass, findRequest.filter, Sorter.NONE, Sublist.NONE,
findRequest.facets, user, findRequest.onlyActive, new ArrayList<>());
return ApiUtils.okResponse(
new RodaStreamingOutput(new FacetsCSVOutputStream(result.getFacetResults(), findRequest.filename, delimiter))
.toStreamResponse());
} else {
final IterableIndexResult<T> result = Browser.findAll(returnClass, findRequest.filter, findRequest.sorter,
findRequest.sublist, findRequest.facets, user, findRequest.onlyActive, new ArrayList<>());
return ApiUtils
.okResponse(new RodaStreamingOutput(new ResultsCSVOutputStream<>(result, findRequest.filename, delimiter))
.toStreamResponse());
}
}
/**
* Return the {@link Class} with the specified class name.
*
* @param className
* the fully qualified name of the desired class.
* @param <T>
* the type of {@link Class}.
* @return the {@link Class} with the specified class name.
* @throws RequestNotValidException
* if the class name is not valid.
*/
@SuppressWarnings("unchecked")
private <T> Class<T> getClass(final String className) throws RequestNotValidException {
try {
return (Class<T>) Class.forName(className);
} catch (final ClassNotFoundException e) {
throw new RequestNotValidException(String.format("Invalid value for classToReturn '%s'", className), e);
}
}
}