/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.core.rest.resources;
import java.io.IOException;
import java.io.Reader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.commons.Statistics;
import org.eclipse.skalli.model.ByProjectIdComparator;
import org.eclipse.skalli.model.ByProjectNameComparator;
import org.eclipse.skalli.model.ByUUIDComparator;
import org.eclipse.skalli.model.ExtensionEntityBase;
import org.eclipse.skalli.model.Issue;
import org.eclipse.skalli.model.NoSuchPropertyException;
import org.eclipse.skalli.model.Project;
import org.eclipse.skalli.model.Severity;
import org.eclipse.skalli.model.ValidationException;
import org.eclipse.skalli.services.entity.EntityServices;
import org.eclipse.skalli.services.extension.ExtensionService;
import org.eclipse.skalli.services.extension.ExtensionServices;
import org.eclipse.skalli.services.extension.PropertyMapper;
import org.eclipse.skalli.services.extension.rest.ResourceBase;
import org.eclipse.skalli.services.extension.rest.ResourceRepresentation;
import org.eclipse.skalli.services.extension.rest.RestException;
import org.eclipse.skalli.services.extension.rest.RestUtils;
import org.eclipse.skalli.services.permit.Permits;
import org.eclipse.skalli.services.project.ProjectService;
import org.eclipse.skalli.services.search.QueryParseException;
import org.eclipse.skalli.services.search.SearchQuery;
import org.eclipse.skalli.services.search.SearchResult;
import org.eclipse.skalli.services.search.SearchUtils;
import org.restlet.data.Form;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.Put;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProjectsResource extends ResourceBase {
private static final Logger LOG = LoggerFactory.getLogger(ProjectsResource.class);
private static final String ID_PREFIX = "rest:api/projects:"; //$NON-NLS-1$
private static final String ERROR_ID_UNEXPECTED = ID_PREFIX + "00"; //$NON-NLS-1$
private static final String ERROR_ID_IO_ERROR = ID_PREFIX + "10"; //$NON-NLS-1$
private static final String ERROR_ID_INVALID_QUERY = ID_PREFIX + "20"; //$NON-NLS-1$
private static final String ERROR_ID_VALIDATION_FAILED = ID_PREFIX + "/{0}:30"; //$NON-NLS-1$
private static final String ERROR_ID_UNSUPPORTED_PROPERTY_TYPE = ID_PREFIX + ":40"; //$NON-NLS-1$
private static final String ERROR_ID_PARSING_ERROR = ID_PREFIX + ":50"; //$NON-NLS-1$
@Get
public Representation retrieve() {
if (!Permits.isAllowed(getAction(), getPath())) {
return createUnauthorizedRepresentation();
}
if (!isSupportedMediaType()) {
setStatus(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE);
return null;
}
try {
RestSearchQuery queryParams = new RestSearchQuery(getQueryAsForm());
Projects projects = getProjects(queryParams);
setStatus(Status.SUCCESS_OK);
return createProjectsResourceRepresentation(projects, queryParams);
} catch (QueryParseException e) {
return createErrorRepresentation(Status.CLIENT_ERROR_BAD_REQUEST, ERROR_ID_INVALID_QUERY,
"Invalid query \"?{0}\": {1}", getQueryString(), e.getMessage()); //$NON-NLS-1$
}
}
private Representation createProjectsResourceRepresentation(Projects projects, RestSearchQuery queryParams) {
if (enforceOldStyleConverters()) {
return new ResourceRepresentation<Projects>(projects,
new ProjectsConverter(getHost(), queryParams.getExtensions(), queryParams.getStart()));
}
return new ResourceRepresentation<Projects>(getResourceContext(), projects,
new ProjectsConverter(queryParams.getExtensions(), queryParams.getStart()));
}
private Projects getProjects(SearchQuery queryParams) throws QueryParseException {
List<Project> projects = null;
if (queryParams.isQueryAll()) {
ProjectService projectService = ((ProjectService)EntityServices.getByEntityClass(Project.class));
projects = projectService.getProjects(getComparator(queryParams));
} else {
SearchResult<Project> searchResult = SearchUtils.searchProjects(queryParams);
Statistics.getDefault().trackSearch(Permits.getLoggedInUser(), searchResult.getQueryString(),
searchResult.getResultCount(), searchResult.getDuration());
projects = searchResult.getEntities();
}
int size = projects.size();
int fromIndex = Math.min(queryParams.getStart(), size) ;
int toIndex = Math.min(fromIndex + queryParams.getCount(), size);
Projects result = new Projects();
if (StringUtils.isBlank(queryParams.getProperty())) {
// if there is no property filter, just add the requested subset
// of projects to the result and quit
result.addAll(projects.subList(fromIndex, toIndex));
} else {
// if there is a property filter, add only those projects
// to the result that match the filter
filterProjects(result, projects, queryParams, fromIndex, toIndex);
}
return result;
}
private Comparator<Project> getComparator(SearchQuery queryParams) {
switch (queryParams.getOrderBy()) {
case UUID:
return new ByUUIDComparator();
case PROJECT_ID:
return new ByProjectIdComparator();
case PROJECT_NAME:
return new ByProjectNameComparator();
default:
return null;
}
}
private void filterProjects(Projects result, List<Project> projects,
SearchQuery queryParams, int fromIndex, int toIndex) throws QueryParseException {
Class<? extends ExtensionEntityBase> extClass = null;
if (queryParams.isExtension()) {
String shortName = queryParams.getShortName();
ExtensionService<?> extService = ExtensionServices.getByShortName(shortName);
extClass = extService != null? extService.getExtensionClass() : null;
// always render extensions that are referenced in the property query
queryParams.addExtension(shortName);
}
int index = 0;
for (Project project : projects) {
if (index >= toIndex) {
break;
}
ExtensionEntityBase ext = extClass != null ? project.getExtension(extClass) : project;
if (ext == null) {
continue;
}
if (matchesPropertyQuery(project, ext, queryParams)) {
if (index >= fromIndex) {
result.addProject(project);
}
++index;
}
}
}
boolean matchesPropertyQuery(Project project, ExtensionEntityBase ext, SearchQuery queryParams)
throws QueryParseException {
Object propertyValue = null;
try {
propertyValue = ext.getProperty(queryParams.getExpressions());
} catch (NoSuchPropertyException e) {
throw new QueryParseException(MessageFormat.format(
"Failed to retrieve property \"{0}\" of extension \"{1}\" of project \"{2}\"",
e.getExpression(), queryParams.getShortName(), project), e);
}
if (queryParams.isNegate()) {
return isBlank(propertyValue);
}
return matches(propertyValue, queryParams.getPattern());
}
static boolean isBlank(Object o) {
if (o == null) {
return true;
}
if (o instanceof Iterable) {
return !((Iterable<?>)o).iterator().hasNext();
}
return StringUtils.isBlank(o.toString());
}
static boolean matches(Object propertyValue, Pattern pattern) {
if (propertyValue == null) {
return false;
}
if (propertyValue instanceof Iterable) {
Iterator<?> it = ((Iterable<?>)propertyValue).iterator();
while (it.hasNext()) {
Matcher matcher = pattern.matcher(it.next().toString());
if (matcher.matches()) {
return true;
}
}
} else {
Matcher matcher = pattern.matcher(propertyValue.toString());
if (matcher.matches()) {
return true;
}
}
return false;
}
@Post
public Representation create(Representation entity) {
if (!Permits.isAllowed(getAction(), getPath())) {
return createUnauthorizedRepresentation();
}
String id = getHeader("Slug", null); //$NON-NLS-1$
if (StringUtils.isBlank(id)) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Slug header missing");
return null;
}
ProjectService projectService = ((ProjectService)EntityServices.getByEntityClass(Project.class));
Project project = projectService.getProject(id);
if (project != null) {
setStatus(Status.CLIENT_ERROR_FORBIDDEN,
MessageFormat.format("Project {0} already exists", id));
return null;
}
// start with an initial project
project = new Project();
if (entity != null && entity.isAvailable()) {
if (!isSupportedMediaType()) {
setStatus(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE);
return null;
}
try {
Reader entityReader = entity.getReader();
if (entityReader == null) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST, "Request entity required");
return null;
}
project = new ResourceRepresentation<Project>(getResourceContext(),
new ProjectConverter()).read(entityReader);
} catch (RestException e) {
String errorId = MessageFormat.format(ERROR_ID_PARSING_ERROR, id);
return createParseErrorRepresentation(errorId, e);
} catch (IOException e) {
String errorId = MessageFormat.format(ERROR_ID_IO_ERROR, id);
return createIOErrorRepresentation(errorId, e);
}
}
// always use the symbolic name from the Slug header and
// ignore the id attribute from the request body
project.setProjectId(id);
try {
projectService.persist(project, Permits.getLoggedInUser());
} catch (ValidationException e) {
String errorId = MessageFormat.format(ERROR_ID_VALIDATION_FAILED, id);
return createValidationFailedRepresentation(errorId, id, e);
}
getResponse().setLocationRef(MessageFormat.format(
"{0}{1}{2}", getHost(), RestUtils.URL_PROJECTS, project.getUuid().toString()));
setStatus(Status.SUCCESS_CREATED);
return null;
}
@Put
public Representation update(Representation entity) {
if (!Permits.isAllowed(getAction(), getPath())) {
return createUnauthorizedRepresentation();
}
Reference resourceRef = getRequest().getResourceRef();
Form form = resourceRef.getQueryAsForm();
RestSearchQuery queryParams = null;
try {
queryParams = new RestSearchQuery(form, entity);
} catch (QueryParseException e) {
return createErrorRepresentation(Status.CLIENT_ERROR_BAD_REQUEST, ERROR_ID_INVALID_QUERY,
"Invalid query \"?{0}\": {1}", form.getQueryString(), e.getMessage()); //$NON-NLS-1$
} catch (IOException e) {
return createIOErrorRepresentation(ERROR_ID_IO_ERROR, e);
}
Projects projects = null;
try {
projects = getProjects(queryParams);
} catch (QueryParseException e) {
return createErrorRepresentation(Status.CLIENT_ERROR_BAD_REQUEST, ERROR_ID_INVALID_QUERY,
"Invalid query \"?{0}\": {1}", form.getQueryString(), e.getMessage()); //$NON-NLS-1$
}
ProjectService projectService = ((ProjectService)EntityServices.getByEntityClass(Project.class));
Projects updatedProjects = new Projects();
Representation representation = updateProperties(projects, updatedProjects, queryParams, projectService);
if (representation != null) {
return representation;
}
if (queryParams.doPersist()) {
representation = persistUpdatedProjects(updatedProjects, projectService);
if (representation != null) {
return representation;
}
}
setStatus(Status.SUCCESS_OK);
return createProjectsResourceRepresentation(updatedProjects, queryParams);
}
private Representation persistUpdatedProjects(Projects projects, ProjectService projectService) {
String loggedInUser = Permits.getLoggedInUser();
try {
for (Project project : projects.getProjects()) {
projectService.persist(project, loggedInUser);
}
} catch (ValidationException e) {
// should never happen since we validated all projects beforehand, but you never know
return createUnexpectedErrorRepresentation(ERROR_ID_UNEXPECTED, e);
}
return null;
}
private Representation updateProperties(Projects projects, Projects updatedProjects,
RestSearchQuery queryParams, ProjectService projectService) {
Class<? extends ExtensionEntityBase> extensionClass = null;
if (queryParams.isExtension()) {
String shortName = queryParams.getShortName();
ExtensionService<?> extService = ExtensionServices.getByShortName(shortName);
extensionClass = extService != null? extService.getExtensionClass() : null;
}
try {
for (Project project : projects.getProjects()) {
Project loadedProject = projectService.loadEntity(Project.class, project.getUuid());
if (loadedProject == null) {
//should never happen, just a precaution
continue;
}
updateProperty(loadedProject, extensionClass, queryParams);
SortedSet<Issue> issues = projectService.validate(loadedProject, Severity.FATAL);
if (!issues.isEmpty()) {
String errorId = MessageFormat.format(ERROR_ID_VALIDATION_FAILED, loadedProject.getProjectId());
return createValidationFailedRepresentation(errorId, loadedProject.getProjectId(), issues);
}
updatedProjects.addProject(loadedProject);
}
} catch (UnsupportedOperationException e) {
String message = MessageFormat.format("Failed to update property \"{0}\": unsupported type",
queryParams.getProperty());
LOG.warn(MessageFormat.format("{0} ({1})", message, ERROR_ID_UNSUPPORTED_PROPERTY_TYPE));
return createErrorRepresentation(Status.CLIENT_ERROR_BAD_REQUEST,
ERROR_ID_UNSUPPORTED_PROPERTY_TYPE, message);
} catch (RuntimeException e) {
// NoSuchPropertyException, PropertyUpdateExeption: should not happen for correctly
// implemented extensions, but you never know
return createUnexpectedErrorRepresentation(ERROR_ID_UNEXPECTED, e);
}
return null;
}
@SuppressWarnings("unchecked")
private void updateProperty(Project project, Class<? extends ExtensionEntityBase> extensionClass,
RestSearchQuery queryParams) {
String propertyName = queryParams.getPropertyName();
ExtensionEntityBase extension = extensionClass != null ? project.getExtension(extensionClass) : project;
if (extension != null) {
Object propertyValue = extension.getProperty(propertyName);
if (propertyValue instanceof Collection) {
try {
updateCollectionProperty(project, extension, (Collection<String>) propertyValue, queryParams);
} catch (ClassCastException e) {
throw new UnsupportedOperationException(MessageFormat.format(
"\"{0}\" is not a collection of strings", propertyName));
}
} else {
updateStringProperty(project, extension, propertyValue, queryParams);
}
}
}
private void updateStringProperty(Project project, ExtensionEntityBase extension,
Object propertyValue, RestSearchQuery queryParams) {
String newValue = propertyValue != null?
PropertyMapper.convert(propertyValue.toString(), queryParams.getPattern(),
queryParams.getTemplate(), project) : null;
extension.setProperty(queryParams.getPropertyName(), newValue);
}
private void updateCollectionProperty(Project project,
ExtensionEntityBase extension, Collection<String> propertyValues, RestSearchQuery queryParams) {
Collection<String> newValues = new ArrayList<String>(propertyValues.size());
for (String propertyValue : propertyValues) {
Matcher matcher = queryParams.getPattern().matcher(propertyValue);
if (matcher.matches()) {
propertyValue = PropertyMapper.convert(propertyValue, queryParams.getPattern(), queryParams.getTemplate(), project);
}
newValues.add(propertyValue);
}
extension.setProperty(queryParams.getPropertyName(), newValues);
}
}