/******************************************************************************* * 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.services.configuration; import java.io.IOException; import java.text.MessageFormat; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.SortedSet; import org.eclipse.skalli.commons.CollectionUtils; import org.eclipse.skalli.model.Issue; import org.eclipse.skalli.services.Services; import org.eclipse.skalli.services.extension.rest.ResourceBase; import org.eclipse.skalli.services.extension.rest.ResourceRepresentation; import org.eclipse.skalli.services.permit.Permits; import org.restlet.data.Status; import org.restlet.representation.Representation; import org.restlet.resource.Get; import org.restlet.resource.Put; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.Converter; /** * Base class for REST resources representing configurations. Derived classes must * implement {@link #getConfigClass()} providing the configuration class this REST resource * is associated with. * <br> * Note, the class that represents the configuration in the underlying storage may be different * from that representing the REST resource. In that case, derived classes should overwrite the * {@link #storeConfig(ConfigurationService, Object, Map)} and {@link #readConfig(ConfigurationService, Map)} * methods to provide the necessary mapping. * * @param <T> the configuration class associated with this REST resource. */ public abstract class ConfigResourceBase<T> extends ResourceBase { protected static final String ID_PREFIX = "rest:api/config/{0}:"; //$NON-NLS-1$ protected static final String ERROR_ID_UNEXPECTED = ID_PREFIX + "00"; //$NON-NLS-1$ protected static final String ERROR_ID_IO_ERROR = ID_PREFIX + "10"; //$NON-NLS-1$ protected static final String ERROR_ID_NO_CONFIGURATION_SERVICE_AVAILABLE = ID_PREFIX + "20"; //$NON-NLS-1$ protected static final String ERROR_VALIDATION_FAILED = ID_PREFIX + "30"; //$NON-NLS-1$ protected static final String WARN_ISSUES = ID_PREFIX + "40"; //$NON-NLS-1$ protected static final String ERROR_INVALID_SYNTAX = ID_PREFIX + "50"; //$NON-NLS-1$ protected static final String ERROR_ID_PROTECT_FAILED = ID_PREFIX + "60"; //$NON-NLS-1$ /** * Returns the configuration class represented by this REST resource. */ protected abstract Class<T> getConfigClass(); /** * Returns additional classes (e.g. classes referenced in fields of the configuration class) * that should be parsed for {@link com.thoughtworks.xstream.annotations XStream annotations} * during marshaling/unmarshaling of this REST resource. * <br> * This implementation always returns an empty list. * * @return a list of additional classes, or an empty list. */ protected List<Class<?>> getAdditionalConfigClasses() { return Collections.emptyList(); }; /** * Returns additional {@link #Converter XStream converters} to apply during marshaling/unmarshaling * of this REST resource. * <br> * This implementation always returns an empty list. * * @return a list of converters, or an empty list. */ protected List<Converter> getAdditionalConverters() { return Collections.emptyList(); } /** * Reads the configuration associated with this REST resource from the underlying storage. * This implementation assumes that the class of the configuration in the storage * is the same that represents this REST resource (i.e. {@link #getConfigClass()}). * * @param configService the configuration service to read from. * @param requestAttributes optional attributes extracted from the REST request. * * @return the configuration stored in the underlying storage service, or <code>null</code> * if no such configuration exists. */ protected T readConfig(ConfigurationService configService, Map<String, Object> requestAttributes) { return configService.readConfiguration(getConfigClass()); } /** * Stores the configuration associated with this REST resource in the underlying storage. * This implementation assumes that the class of the configuration in the storage * is the same that represents this REST resource (i.e. {@link #getConfigClass()}). * * @param configService the configuration service to write to. * @param config the configuration to store. * @param requestAttributes optional attributes extracted from the REST request. */ protected void storeConfig(ConfigurationService configService, T config, Map<String, Object> requestAttributes) { configService.writeConfiguration(config); } /** * Returns a preconfigured {@link XStream} instance suitable for marshaling/unmarshaling of * the configuration represented by this REST resource to/from the underlying storage. * <br> * Configures necessary class loaders and registers additional {@link #getAdditionalConverters() * XStream converters}. Registers additional classes that should be parsed for * {@link com.thoughtworks.xstream.annotations XStream annotations}. * * @return a preconfigured XStream instance, never <code>null</code>. */ protected XStream getXStream() { XStream xstream = new XStream(); xstream.setClassLoader(getClass().getClassLoader()); xstream.processAnnotations(getConfigClass()); for (Class<?> additionalClass : getAdditionalConfigClasses()) { xstream.processAnnotations(additionalClass); } for (Converter converter : getAdditionalConverters()) { xstream.registerConverter(converter); } return xstream; } /** * Validates the given configuration prior to storage. * <br> * This implementation always returns an empty set of issues. * * @param configuration the configuration to validate. * @param loggedInUser the unique identifier of the currently logged in user, * or <code>null</code> if the user is anonymous. * * @return the issues found during the validation, or an empty set. */ protected SortedSet<Issue> validate(T configuration, String loggedInUser) { return CollectionUtils.emptySortedSet(); } /** * Returns the currently active configuration service. This method must * not be called directly and not be overwritten except for testing purposes. */ protected ConfigurationService getConfigService() { return Services.getService(ConfigurationService.class); } /** * Handler for GET requests routed to this REST resource. * <br> * Possible response codes: * <ul> * <li>200 OK — requested configuration found and returned in response body.</li> * <li>403 FORBIDDEN — the logged in user has not the necessary permits to access this resource.</li> * <li>404 NOT FOUND — requested configuration does not exist or could not be read from storage.</li> * <li>500 SERVER ERROR with error id <tt>"rest:api/config/<id>:20"</tt> — no * configuration service available.</li> * <li>500 SERVER ERROR with error id <tt>"rest:api/config/<id>:60"</tt> — obfuscating * of protected fields in the response failed.</li> * </ul> * * @return the REST representation of the requested configuration, or an error representation * if the configuration could not be read, or <code>null</code> if the configuration does not exist. */ @Get public final Representation retrieve() { if (!Permits.isAllowed(getAction(), getPath())) { return createUnauthorizedRepresentation(); } ConfigurationService configService = getConfigService(); if (configService == null) { String errorId = MessageFormat.format(ERROR_ID_NO_CONFIGURATION_SERVICE_AVAILABLE, getPath()); return createServiceUnavailableRepresentation(errorId, "Configuration Service"); } T config = readConfig(configService, getRequestAttributes()); if (config == null) { setStatus(Status.CLIENT_ERROR_NOT_FOUND, MessageFormat.format("Configuration {0} not found", getPath())); return null; } try { Protector.protect(config, getAdditionalConfigClasses()); } catch (Exception e) { String errorId = MessageFormat.format(ERROR_ID_PROTECT_FAILED, getPath()); createUnexpectedErrorRepresentation(errorId, e); } ResourceRepresentation<T> representation = new ResourceRepresentation<T>(config); representation.setXStream(getXStream()); return representation; } /** * Handler for PUT requests routed to this REST resource. * <br> * Possible response codes: * <ul> * <li>200 OK — configuration has been stored successfully, but some issues have been found * during validation. Response body contains an error representation with id * <tt>"rest:api/config/<id>:40"</tt>. The error detail message lists the found issues.</li> * <li>204 NO CONTENT — configuration is valid and has been stored successfully.</li> * <li>400 BAD REQUEST with error id <tt>"rest:api/config/<id>:30"</tt> — * configuration is not valid. The error detail message lists the found fatal issues.</li> * <li>400 BAD REQUEST with error id <tt>"rest:api/config/<id>:50"</tt> — * request body is not parsable due to malformed syntax.</li> * <li>403 FORBIDDEN — the logged in user has not the necessary permits to access this resource.</li> * <li>500 SERVER ERROR with error id <tt>"rest:api/config/<id>:00"</tt> — a severe * error occured during request processing. See server log for details.</li> * <li>500 SERVER ERROR with error id <tt>"rest:api/config/<id>:10"</tt> — an i/o error * occured during request processing. See server log for details.</li> * <li>500 SERVER ERROR with error id <tt>"rest:api/config/<id>:20"</tt> — no * configuration service available.</li> * </ul> * @param entity the request entity to be interpreted as configuration. * @return an error representation, if the configuration could not be stored or issues have * been found, <code>null</code> otherwise. */ @Put public final Representation store(Representation entity) { if (!Permits.isAllowed(getAction(), getPath())) { return createUnauthorizedRepresentation(); } ConfigurationService configService = getConfigService(); if (configService == null) { String errorId = MessageFormat.format(ERROR_ID_NO_CONFIGURATION_SERVICE_AVAILABLE, getPath()); return createServiceUnavailableRepresentation(errorId, "Configuration Service"); } try { XStream xstream = getXStream(); T config = getConfigClass().cast(xstream.fromXML(entity.getText())); SortedSet<Issue> issues = validate(config, Permits.getLoggedInUser()); if (Issue.hasFatalIssues(issues)) { String errorId = MessageFormat.format(ERROR_VALIDATION_FAILED, getPath()); return createErrorRepresentation(Status.CLIENT_ERROR_BAD_REQUEST, errorId, Issue.getMessage("Invalid configuration", issues)); } else { storeConfig(configService, config, getRequestAttributes()); if (issues.size() > 0) { String errorId = MessageFormat.format(WARN_ISSUES, getPath()); return createErrorRepresentation(Status.SUCCESS_OK, errorId, Issue.getMessage("Configuration stored but has the following issues: ", issues)); } setStatus(Status.SUCCESS_NO_CONTENT, "Configuration successfully stored"); return null; } } catch (XStreamException e) { String errorId = MessageFormat.format(ERROR_INVALID_SYNTAX, getPath()); return createErrorRepresentation(Status.CLIENT_ERROR_BAD_REQUEST, errorId, MessageFormat.format( "Request could not be understood due to malformed syntax: {0}", e.getMessage())); } catch (IOException e) { String errorId = MessageFormat.format(ERROR_ID_IO_ERROR, getPath()); return createIOErrorRepresentation(errorId, e); } catch (Exception e) { String errorId = MessageFormat.format(ERROR_ID_UNEXPECTED, getPath()); return createUnexpectedErrorRepresentation(errorId, e); } } }