package com.sixsq.slipstream.module;
/*
* +=================================================================+
* SlipStream Server (WAR)
* =====
* Copyright (C) 2013 SixSq Sarl (sixsq.com)
* =====
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* -=================================================================-
*/
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.logging.Level;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import com.sixsq.slipstream.event.Event;
import com.sixsq.slipstream.persistence.*;
import com.sixsq.slipstream.run.RunsQueryParameters;
import com.sixsq.slipstream.util.*;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.json.JSONObject;
import org.json.XML;
import org.restlet.Request;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.ext.fileupload.RestletFileUpload;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.Delete;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.Put;
import org.restlet.resource.ResourceException;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.sixsq.slipstream.connector.ConnectorFactory;
import com.sixsq.slipstream.exceptions.BadlyFormedElementException;
import com.sixsq.slipstream.exceptions.ConfigurationException;
import com.sixsq.slipstream.exceptions.SlipStreamClientException;
import com.sixsq.slipstream.exceptions.Util;
import com.sixsq.slipstream.exceptions.ValidationException;
import com.sixsq.slipstream.factory.ParametersFactory;
import com.sixsq.slipstream.resource.ParameterizedResource;
import com.sixsq.slipstream.run.RunViewList;
/**
* Unit test see
*
* @see ModuleResourceTest
*
*/
public class ModuleResource extends ParameterizedResource<Module> {
private ModuleCategory category = null;
public static final String COPY_SOURCE_FORM_PARAMETER_NAME = "source_uri";
public static final String COPY_TARGET_FORM_PARAMETER_NAME = "target_name";
@Override
protected String getPageRepresentation() {
return "module";
}
private String toXmlString() {
checkCanGet();
Module prepared = null;
try {
prepared = prepareForSerialization();
} catch (ValidationException e) {
throwClientValidationError(e.getMessage());
} catch (ConfigurationException e) {
Util.throwConfigurationException(e);
}
return XmlUtil.normalize(prepared);
}
@Get("xml")
public Representation toXml() {
String result = toXmlString();
return new StringRepresentation(result, MediaType.APPLICATION_XML);
}
@Get("json")
public Representation toJson() {
String xml = toXmlString();
JSONObject obj = XML.toJSONObject(xml);
return new StringRepresentation(obj.toString(), MediaType.APPLICATION_JSON);
}
@Post("form")
public void copyTo(Representation entity) throws ValidationException {
Form form = new Form(entity);
if (!isExisting()) {
throwClientForbiddenError("Target project doesn't exist: " + getParameterized().getName());
}
String sourceUri = form.getFirstValue(COPY_SOURCE_FORM_PARAMETER_NAME);
if (sourceUri == null) {
throwClientBadRequest("Missing source uri form parameter");
}
String targetName = form.getFirstValue(COPY_TARGET_FORM_PARAMETER_NAME);
if (targetName == null) {
throwClientBadRequest("Missing target name form parameter");
}
Module source = Module.load(sourceUri);
if (source == null) {
throwClientBadRequest("Unknown source module: " + sourceUri);
}
if (!source.getAuthz().canGet(getUser())) {
throwClientForbiddenError("You do not have read rights on the source module: " + source.getName());
}
String targetFullName = getParameterized().getName() + "/" + targetName;
String targetUri = Module.constructResourceUri(targetFullName);
Module target = Module.load(targetUri);
if (target != null) {
throwClientForbiddenError("Target module already exists: " + targetUri);
}
if (!getParameterized().getAuthz().canCreateChildren(getUser())) {
throwClientForbiddenError("You do not have rights to create modules in this project");
}
try {
target = source.copy();
target.getAuthz().setUser(getUser().getName());
target.getAuthz().clear();
target.setName(targetFullName);
target.store();
} catch (ValidationException e) {
throwClientValidationError(e.getMessage());
}
postEventCopied(targetFullName);
String absolutePath = RequestUtil.constructAbsolutePath(getRequest(), "/" + target.getResourceUri());
getResponse().setLocationRef(absolutePath);
getResponse().setStatus(Status.SUCCESS_CREATED);
}
@Delete
public void deleteModule() {
if (!canDelete()) {
throwClientForbiddenError();
}
getParameterized().setDeleted(true);
getParameterized().store(false);
Module latest = Module.loadLatest(getParameterized().getResourceUri());
try {
if (latest == null) {
redirectToParent();
} else {
redirectToLatest(latest);
}
} catch (ValidationException e) {
throwClientConflicError(e.getMessage());
}
postEventDeleted();
}
private void redirectToParent() throws ValidationException {
String resourceUri = getParameterized().getResourceUri();
String parentResourceUri = ModuleUriUtil.extractParentUriFromResourceUri(resourceUri);
String absolutePath = RequestUtil.constructAbsolutePath(getRequest(), "/" + parentResourceUri);
getResponse().setLocationRef(absolutePath);
getResponse().setStatus(Status.SUCCESS_NO_CONTENT);
}
private void redirectToLatest(Module latest) {
String absolutePath = RequestUtil.constructAbsolutePath(getRequest(), "/" + latest.getResourceUri());
getResponse().setLocationRef(absolutePath);
getResponse().setStatus(Status.SUCCESS_NO_CONTENT);
}
@Put("form")
public void updateOrCreateFromForm(Representation entity) throws ResourceException {
if (entity == null) {
throwClientBadRequest("Empty form");
}
Module module = null;
try {
module = (Module) processEntityAsForm(entity);
} catch (ValidationException e) {
throwClientValidationError(e.getMessage());
}
updateOrCreate(module);
String absolutePath = RequestUtil.constructAbsolutePath(getRequest(), "/" + module.getResourceUri());
getResponse().setLocationRef(absolutePath);
if (!isExisting()) {
getResponse().setStatus(Status.SUCCESS_CREATED);
}
setEmptyEntity(MediaType.APPLICATION_WWW_FORM);
}
@Put("multipart")
public void updateOrCreateFromXmlMultipart(Representation entity) throws ResourceException {
Module module = xmlMultipartToModule();
updateOrCreate(module);
String absolutePath = RequestUtil.constructAbsolutePath(getRequest(), "/" + module.getResourceUri());
getResponse().setLocationRef(absolutePath);
if (!isExisting()) {
getResponse().setStatus(Status.SUCCESS_CREATED);
}
setEmptyEntity(MediaType.MULTIPART_ALL);
}
private Module xmlMultipartToModule() {
return xmlToModule(extractXmlMultipart());
}
private String extractXmlMultipart() {
RestletFileUpload upload = new RestletFileUpload(
new DiskFileItemFactory());
List<FileItem> items;
Request request = getRequest();
try {
items = upload.parseRequest(request);
} catch (FileUploadException e) {
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST,
e.getMessage());
}
String module = null;
for (FileItem fi : items) {
if (fi.getName() != null) {
module = getContent(fi);
}
}
if (module == null) {
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST,
"the file is empty");
}
return module;
}
@Put("xml")
public void updateOrCreateFromXml(Representation entity)
throws ResourceException {
Module module = xmlToModule();
updateOrCreate(module);
if (!isExisting()) {
getResponse().setStatus(Status.SUCCESS_CREATED);
}
getResponse().setLocationRef("/" + module.getResourceUri());
setEmptyEntity(MediaType.APPLICATION_XML);
}
private Module xmlToModule() {
Module module = xmlToModule(extractXml());
// Reset user
module.getAuthz().setUser(getUser().getName());
return module;
}
public static Module xmlToModule(String xml) {
String denormalized = XmlUtil.denormalize(xml);
Class<? extends Module> moduleClass = getModuleClass(denormalized);
Module module = null;
try {
module = (Module) SerializationUtil.fromXml(denormalized, moduleClass);
} catch (SlipStreamClientException e) {
e.printStackTrace();
throwClientBadRequest("Invalid xml module: " + e.getMessage());
}
module.postDeserialization();
return module;
}
private String extractXml() {
return getRequest().getEntityAsText();
}
private void updateOrCreate(Module module) {
checkCanPut();
try {
module.validate();
} catch (ValidationException ex) {
throw new ResourceException(Status.CLIENT_ERROR_CONFLICT, ex);
}
String moduleUri = null;
String targetUri = null;
try {
moduleUri = ModuleUriUtil.extractVersionLessResourceUri(module
.getResourceUri());
targetUri = ModuleUriUtil
.extractVersionLessResourceUri(getTargetParameterizeUri());
} catch (ValidationException e) {
throwClientValidationError(e.getMessage());
}
try {
checkConsistentModule(moduleUri, targetUri);
} catch (ValidationException e) {
throwClientValidationError(e.getMessage());
}
module.unpublish();
module.store();
if (isExisting()) {
postEventUpdated();
} else {
postEventCreated();
}
}
private void checkConsistentModule(String moduleUri, String targetUri)
throws ValidationException {
if (!targetUri.equals(moduleUri)) {
throwClientBadRequest("The uploaded module does not correspond to the target module uri");
}
}
@SuppressWarnings("unchecked")
private static Class<? extends Module> getModuleClass(String moduleAsXml) {
String category = null;
try {
category = extractCategory(moduleAsXml);
} catch (ParserConfigurationException e) {
e.printStackTrace();
throwServerError("Failed to parse module");
} catch (SAXException e) {
e.printStackTrace();
throwClientBadRequest("Invalid xml document");
} catch (IOException e) {
e.printStackTrace();
throwServerError("Failed to parse module");
}
String className = "com.sixsq.slipstream.persistence." + category
+ "Module";
Class<? extends Module> moduleClass = null;
try {
moduleClass = (Class<? extends Module>) Class.forName(className);
} catch (ClassNotFoundException e) {
throwClientBadRequest("Unknown category");
}
return moduleClass;
}
protected static String extractCategory(String moduleAsXml)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db;
db = dbf.newDocumentBuilder();
StringReader reader = new StringReader(moduleAsXml);
Document document = db.parse(new InputSource(reader));
return document.getDocumentElement().getAttributes()
.getNamedItem("category").getNodeValue();
}
protected String getContent(FileItem fi) {
return fi.getString();
}
@Override
protected Module loadParameterized(String targetParameterizedUri)
throws ValidationException {
return loadModule(targetParameterizedUri);
}
public Module loadModule(String targetParameterizedUri)
throws ValidationException {
Module module = Module.load(targetParameterizedUri);
if (module != null) {
if (module.getCategory() == ModuleCategory.Project) {
List<ModuleView> children = Module.viewList(module
.getResourceUri());
((ProjectModule) module).setChildren(children);
}
}
return module;
}
@Override
protected void setIsEdit() throws ConfigurationException,
ValidationException {
super.setIsEdit();
if (isExisting()) {
// Add connector information for the transformation
List<String> serviceCloudNames = ConnectorFactory
.getCloudServiceNamesList();
serviceCloudNames.add(CloudImageIdentifier.DEFAULT_CLOUD_SERVICE);
getParameterized().setCloudNames(
serviceCloudNames.toArray(new String[0]));
}
}
private Metadata processEntityAsForm(Representation entity)
throws ValidationException {
// Add the default module parameters if the module is not new,
// to ensure that all mandatory parameters are present.
// This is required to avoid inconsistent modules, for example
// when connectors are added in the configuration
Module previous = getParameterized();
if (previous != null) {
try {
ParametersFactory.addParametersForEditing(previous);
} catch (ValidationException e) {
throwClientConflicError(e.getMessage());
} catch (ConfigurationException e) {
throwConfigurationException(e);
}
}
// This "noop" disrupts some optimization or bug in the
// OpenJDK JVM. If this isn't present, then unit tests
// intermittently fail with complaints that the request
// doesn't contain a form entity. Note that this doesn't
// happen with the Oracle JVM.
//
// If we were sticking with Java in the longterm, this
// would probably warrant more investigation, but the
// kludge is probably sufficient for now.
entity.toString();
Form form = extractFormFromEntity(entity);
ModuleFormProcessor processor = ModuleFormProcessor
.createFormProcessorInstance(getCategory(form), getUser());
try {
processor.processForm(form);
} catch (BadlyFormedElementException e) {
throwClientError(e);
} catch (SlipStreamClientException e) {
throwClientError(e);
}
if (newInQuery() && !isExisting()) {
processor.adjustModule(previous);
}
Module module = processor.getParametrized();
module = copyAllParameters(module);
category = module.getCategory();
module = resetMandatoryParameters(module);
return module;
}
private Module copyAllParameters(Module module) throws ValidationException
{
for (ModuleParameter p : module.getParameterList()) {
module.setParameter(p.copy());
}
return module;
}
private Module resetMandatoryParameters(Module module)
throws ValidationException {
for (ModuleParameter referenceParameter : getOrCreateParameterized(
"reference").getParameterList()) {
ModuleParameter p = module.getParameter(referenceParameter
.getName());
if (p != null) {
referenceParameter.setValue(p.getValue());
}
module.setParameter(referenceParameter);
}
return module;
}
@Override
protected String extractTargetUriFromRequest() {
String module = (String) getRequest().getAttributes().get("module");
int version = extractVersion();
String moduleName = (version == Module.DEFAULT_VERSION ? module
: module + "/" + version);
return Module.constructResourceUri(moduleName);
}
private int extractVersion() {
String v = (String) getRequest().getAttributes().get("version");
return (v == null) ? Module.DEFAULT_VERSION : Integer.parseInt(v);
}
@Override
protected void authorize() {
setCanPut(authorizePut());
if (isExisting()) {
setCanDelete(authorizeDelete());
}
if (isExisting()) {
setCanGet(authorizeGet());
}
if (getParameterized() != null && getParameterized().isDeleted()
&& !getUser().isSuper()) {
throwClientForbiddenError("Module deleted");
}
}
private boolean authorizeGet() {
if (getUser().isSuper() || newTemplateResource()) {
return true;
}
return getParameterized().getAuthz().canGet(getUser());
}
private boolean authorizeDelete() {
if (getUser().isSuper()) {
return true;
}
return getParameterized().getAuthz().canDelete(getUser());
}
protected boolean authorizePut() {
if (newTemplateResource()) {
return false;
}
if (getUser().isSuper()) {
return true;
}
if (newInQuery() && !isExisting()) {
// check parent
String parentResourceUri = null;
try {
parentResourceUri = ModuleUriUtil
.extractParentUriFromResourceUri(getTargetParameterizeUri());
} catch (ValidationException e) {
return false;
}
Module parent = Module.load(parentResourceUri);
if (parent == null) {
// this is the root module. All can put on it (for now)
return true;
} else {
return parent.getAuthz().canCreateChildren(getUser());
}
}
return isExisting() ? Module
.loadLatest(getParameterized().getResourceUri()).getAuthz()
.canPut(getUser()) : true;
}
@Override
protected Module getOrCreateParameterized(String name)
throws ValidationException {
Module module = null;
switch (getCategory()) {
case Project:
module = new ProjectModule(name);
break;
case Image:
module = new ImageModule(name);
break;
case Deployment:
module = new DeploymentModule(name);
break;
default:
throwClientError("Unknown category");
}
module.setAuthz(new Authz(getUser().getName(), module));
ParametersFactory.addParametersForEditing(module);
return module;
}
private ModuleCategory getCategory(Form form) {
return getCategory(form.getFirstValue("category"));
}
private ModuleCategory getCategory() {
if (category == null) {
String c = (String) getRequest().getAttributes().get("category");
category = (c == null) ? ModuleCategory.Project : getCategory(c);
}
return category;
}
private ModuleCategory getCategory(String category) {
ModuleCategory c = null;
try {
c = ModuleCategory.valueOf(category);
} catch (NullPointerException e) {
throwClientError("Missing category attribute");
} catch (IllegalArgumentException e) {
throwClientError("Invalid category attribute, got: " + category);
}
return c;
}
@Override
protected boolean isChooser() {
String c = (String) getRequest().getAttributes().get("chooser");
return c != null;
}
@Override
protected void addParametersForEditing() throws ValidationException, ConfigurationException {
ParametersFactory.addParametersForEditing(getParameterized());
}
@Override
protected Module prepareForSerialization() throws ConfigurationException, ValidationException {
Module module = getParameterized();
// Add runs for this specific module version (will not apply to project)
RunViewList runs = new RunViewList();
RunsQueryParameters parameters = new RunsQueryParameters(getUser(), 0, Run.DEFAULT_LIMIT, null, null, null,
module.getResourceUri(), getActiveOnly());
runs.populate(parameters);
module.setRuns(runs);
return module;
}
private void postEventCreated() {
postEventModule(Event.Severity.medium, "Created by '" + getUser().getName() + "'");
}
private void postEventUpdated() {
postEventModule(Event.Severity.medium, "Updated by '" + getUser().getName() + "'");
}
private void postEventCopied(String target) {
postEventModule(Event.Severity.medium, "Copied to '" + target + "' by '" + getUser().getName() + "'");
}
private void postEventDeleted() {
postEventModule(Event.Severity.high, "Deleted by '" + getUser().getName() + "'");
}
private void postEventModule(Event.Severity severity, String message) {
String resourceRef = null;
try {
resourceRef = ModuleUriUtil.extractVersionLessResourceUri(getTargetParameterizeUri());
} catch (ValidationException e) {
getLogger().log(Level.WARNING, "Failed to generate event for '" + getTargetParameterizeUri() + "'", e);
}
Event.postEvent(resourceRef, severity, message, getUser().getName(), Event.EventType.action);
}
}