/** * Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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. */ package net.roboconf.dm.rest.services.internal.resources.impl; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.ws.rs.Path; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.ops4j.pax.url.mvn.MavenResolver; import com.sun.jersey.core.header.FormDataContentDisposition; import net.roboconf.core.Constants; import net.roboconf.core.model.beans.Application; import net.roboconf.core.model.beans.ApplicationTemplate; import net.roboconf.core.urlresolvers.DefaultUrlResolver; import net.roboconf.core.urlresolvers.IUrlResolver; import net.roboconf.core.urlresolvers.IUrlResolver.ResolvedFile; import net.roboconf.core.utils.Utils; import net.roboconf.dm.management.ManagedApplication; import net.roboconf.dm.management.Manager; import net.roboconf.dm.management.exceptions.AlreadyExistingException; import net.roboconf.dm.management.exceptions.InvalidApplicationException; import net.roboconf.dm.management.exceptions.UnauthorizedActionException; import net.roboconf.dm.rest.services.internal.resources.IManagementResource; import net.roboconf.dm.rest.services.internal.utils.MavenUrlResolver; import net.roboconf.dm.rest.services.internal.utils.RestServicesUtils; /** * @author Vincent Zurczak - Linagora * @author Pierre Bourret - Université Joseph Fourier */ @Path( IManagementResource.PATH ) public class ManagementResource implements IManagementResource { private static final Set<String> SUPPORTED_EXTENSIONS; static { final Set<String> ex = new HashSet<>(); ex.add("jpg"); ex.add("jpeg"); ex.add("gif"); ex.add("png"); ex.add("svg"); SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(ex); } /** * The maximum allowed image file size, in bytes (1MB). */ public static final long MAX_IMAGE_SIZE = 1024 * 1024; private final Logger logger = Logger.getLogger( getClass().getName()); private final Manager manager; private MavenResolver mavenResolver; /** * Constructor. * @param manager the manager */ public ManagementResource( Manager manager ) { this.manager = manager; } /** * @param mavenResolver the mavenResolver to set */ public void setMavenResolver( MavenResolver mavenResolver ) { this.mavenResolver = mavenResolver; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #loadUploadedZippedApplicationTemplate(java.io.InputStream, com.sun.jersey.core.header.FormDataContentDisposition) */ @Override public Response loadUploadedZippedApplicationTemplate( InputStream uploadedInputStream, FormDataContentDisposition fileDetail ) { this.logger.fine( "Request: load application from an uploaded ZIP file (" + fileDetail.getFileName() + ")." ); File tempZipFile = new File( System.getProperty( "java.io.tmpdir" ), fileDetail.getFileName()); Response response; try { // Copy the uploaded ZIP file on the disk Utils.copyStream( uploadedInputStream, tempZipFile ); // Load the application response = loadZippedApplicationTemplate( tempZipFile.toURI().toURL().toString()); } catch( IOException e ) { response = RestServicesUtils.handleException( this.logger, Status.NOT_ACCEPTABLE, "A new application template could not be loaded.", e ).build(); } finally { Utils.closeQuietly( uploadedInputStream ); // We do not need the uploaded file anymore. // In case of success, it was copied in the DM's configuration. Utils.deleteFilesRecursivelyAndQuietly( tempZipFile ); } return response; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #loadZippedApplicationTemplate(java.lang.String) */ @Override public Response loadZippedApplicationTemplate( String url ) { this.logger.fine( "Request: load application from URL " + url + "." ); ResolvedFile resolvedFile = null; File extractionDir = null; Response response; try { // Retrieve the file as a local one if( Utils.isEmptyOrWhitespaces( url )) throw new InvalidApplicationException( "Null or empty URLs are forbidden." ); // Get a resolver... IUrlResolver resolver = this.mavenResolver != null ? new MavenUrlResolver( this.mavenResolver ) : new DefaultUrlResolver(); resolvedFile = resolver.resolve( url ); // Extract the ZIP content extractionDir = new File( System.getProperty( "java.io.tmpdir" ), "roboconf/" + UUID.randomUUID().toString()); Utils.extractZipArchive( resolvedFile.getFile(), extractionDir ); // Load the application template response = loadUnzippedApplicationTemplate( extractionDir.getAbsolutePath()); } catch( InvalidApplicationException e ) { response = RestServicesUtils.handleException( this.logger, Status.NOT_ACCEPTABLE, "A new application template could not be loaded.", e ).build(); } catch( IOException e ) { response = RestServicesUtils.handleException( this.logger, Status.UNAUTHORIZED, "A new application template could not be loaded.", e ).build(); } finally { // We do not need the extracted application anymore. // In case of success, it was copied in the DM's configuration. Utils.deleteFilesRecursivelyAndQuietly( extractionDir ); // If the resolved file did not exist before, delete it as we do not need it anymore if( resolvedFile != null && ! resolvedFile.existedBefore()) Utils.deleteFilesRecursivelyAndQuietly( resolvedFile.getFile()); } return response; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #loadUnzippedApplicationTemplate(java.lang.String) */ @Override public Response loadUnzippedApplicationTemplate( String localFilePath ) { if( localFilePath == null ) localFilePath = "null"; this.logger.fine( "Request: load application from a local file (" + localFilePath + ")." ); Response response; try { ApplicationTemplate tpl = this.manager.applicationTemplateMngr().loadApplicationTemplate( new File( localFilePath )); response = Response.ok().entity( tpl ).build(); } catch( AlreadyExistingException e ) { response = RestServicesUtils.handleException( this.logger, Status.FORBIDDEN, "A new application template could not be loaded.", e ).build(); } catch( InvalidApplicationException e ) { response = RestServicesUtils.handleException( this.logger, Status.NOT_ACCEPTABLE, "A new application template could not be loaded.", e ).build(); } catch( IOException e ) { response = RestServicesUtils.handleException( this.logger, Status.UNAUTHORIZED, "A new application template could not be loaded.", e ).build(); } return response; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #listApplicationTemplates(java.lang.String, java.lang.String) */ @Override public List<ApplicationTemplate> listApplicationTemplates( String exactName, String exactQualifier ) { // Log if( this.logger.isLoggable( Level.FINE )) { if( exactName == null && exactQualifier == null ) { this.logger.fine( "Request: list all the application templates." ); } else { StringBuilder sb = new StringBuilder( "Request: list/find the application templates" ); if( exactName != null ) { sb.append( " with name = " ); sb.append( exactName ); } if( exactQualifier != null ) { if( exactName != null ) sb.append( " and" ); sb.append( " qualifier = " ); sb.append( exactQualifier ); } sb.append( "." ); this.logger.fine( sb.toString()); } } // Search List<ApplicationTemplate> result = new ArrayList<> (); for( ApplicationTemplate tpl : this.manager.applicationTemplateMngr().getApplicationTemplates()) { // Equality is on the name, not on the display name if(( exactName == null || exactName.equals( tpl.getName())) && (exactQualifier == null || exactQualifier.equals( tpl.getQualifier()))) result.add( tpl ); } return result; } /* * (non-Javadoc) * @see net.roboconf.dm.internal.rest.client.exceptions.server.IApplicationWs * #listApplicationTemplates() */ @Override public List<ApplicationTemplate> listApplicationTemplates() { return listApplicationTemplates( null, null ); } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #deleteApplicationTemplate(java.lang.String, java.lang.String) */ @Override public Response deleteApplicationTemplate( String tplName, String tplQualifier ) { String id = tplName + " (qualifier = " + tplQualifier + ")"; this.logger.fine( "Request: delete application template " + id + "." ); Response result = Response.ok().build(); try { this.manager.applicationTemplateMngr().deleteApplicationTemplate( tplName, tplQualifier ); } catch( InvalidApplicationException e ) { result = RestServicesUtils.handleException( this.logger, Status.NOT_FOUND, "Application template " + id + " was not found.", e ).build(); } catch( UnauthorizedActionException | IOException e ) { result = RestServicesUtils.handleException( this.logger, Status.FORBIDDEN, "Application template " + id + " could not be deleted.", e ).build(); } return result; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #createApplication(net.roboconf.core.model.beans.Application) */ @Override public Response createApplication( Application app ) { this.logger.fine( "Request: create application " + app + "." ); Response result; try { String tplName = app.getTemplate() == null ? null : app.getTemplate().getName(); String tplQualifier = app.getTemplate() == null ? null : app.getTemplate().getQualifier(); String appName = app.getDisplayName() != null ? app.getDisplayName() : app.getName(); ManagedApplication ma = this.manager.applicationMngr().createApplication( appName, app.getDescription(), tplName, tplQualifier ); result = Response.ok().entity( ma.getApplication()).build(); } catch( InvalidApplicationException e ) { result = RestServicesUtils.handleException( this.logger, Status.NOT_FOUND, "Application " + app + " references an invalid template.", e ).build(); } catch( AlreadyExistingException e ) { result = RestServicesUtils.handleException( this.logger, Status.FORBIDDEN, "Application " + app + " already exists.", e ).build(); } catch( IOException e ) { result = RestServicesUtils.handleException( this.logger, Status.UNAUTHORIZED, "Application " + app + " could not be created.", e ).build(); } return result; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #listApplications(java.lang.String) */ @Override public List<Application> listApplications( String exactName ) { if( exactName != null ) this.logger.fine( "Request: list/find the application named " + exactName + "." ); else this.logger.fine( "Request: list all the applications." ); List<Application> result = new ArrayList<> (); for( ManagedApplication ma : this.manager.applicationMngr().getManagedApplications()) { // Equality is on the name, not on the display name if( exactName == null || exactName.equals( ma.getName())) result.add( ma.getApplication()); } return result; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #listApplications() */ @Override public List<Application> listApplications() { return listApplications( null ); } /* (non-Javadoc) * @see net.roboconf.dm.internal.rest.client.exceptions.server.IApplicationWs * #deleteApplication(java.lang.String) */ @Override public Response deleteApplication( String applicationName ) { this.logger.fine( "Request: delete application " + applicationName + "." ); Response result = Response.ok().build(); try { ManagedApplication ma = this.manager.applicationMngr().findManagedApplicationByName( applicationName ); if( ma == null ) result = Response.status( Status.NOT_FOUND ).entity( "Application " + applicationName + " was not found." ).build(); else this.manager.applicationMngr().deleteApplication( ma ); } catch( UnauthorizedActionException | IOException e ) { result = RestServicesUtils.handleException( this.logger, Status.FORBIDDEN, "Application " + applicationName + " could not be deleted.", e ).build(); } return result; } /* * (non-Javadoc) * @see net.roboconf.dm.internal.rest.api.IManagementWs * #shutdownApplication(java.lang.String) */ @Override public Response shutdownApplication( String applicationName ) { this.logger.fine( "Request: shutdown application " + applicationName + "." ); Response result = Response.ok().build(); try { ManagedApplication ma = this.manager.applicationMngr().findManagedApplicationByName( applicationName ); if( ma == null ) result = Response.status( Status.NOT_FOUND ).entity( "Application " + applicationName + " was not found." ).build(); else this.manager.instancesMngr().undeployAll( ma, null ); } catch( Exception e ) { result = RestServicesUtils.handleException( this.logger, Status.FORBIDDEN, "Application " + applicationName + " could not be shutdown.", e ).build(); } return result; } /* * (non-Javadoc) * @see net.roboconf.dm.rest.services.internal.resources.IManagementResource * #setImage(java.lang.String, java.lang.String, java.io.InputStream, com.sun.jersey.core.header.FormDataContentDisposition) */ @Override public Response setImage( final String name, final String qualifier, final InputStream image, final FormDataContentDisposition fileDetail) { // Do set the image, and wrap exceptions in a HTTP response. Response response; try { doSetImage(name, qualifier, image, fileDetail); response = Response.noContent().build(); } catch (final NoSuchElementException | IllegalArgumentException e) { response = RestServicesUtils.handleException(this.logger, Status.BAD_REQUEST, e.getMessage(), e).build(); } catch (final IOException e) { response = RestServicesUtils.handleException(this.logger, Status.INTERNAL_SERVER_ERROR, e.getMessage(), e).build(); } return response; } /** * Upload an image for a template/application. * <p> * If an image was already set, it is overridden by the new one. * </p> * @param name the name of the template/application. * @param qualifier the qualifier of the template, or {@code null} for an application. * @param image the uploaded image. * @param fileDetail the image details. * @throws IllegalArgumentException if the image is too large, or is not supported. * @throws NoSuchElementException if the application/template cannot be found. * @throws IOException if the image cannot be stored. */ private void doSetImage( final String name, final String qualifier, final InputStream image, final FormDataContentDisposition fileDetail) throws IOException { // Check image size and extension. final long size = fileDetail.getSize(); final String extension = getFileExtension(fileDetail.getFileName()); if (size > MAX_IMAGE_SIZE) throw new IllegalArgumentException("Image is too large: " + size); if (!SUPPORTED_EXTENSIONS.contains(extension)) throw new IllegalArgumentException("Unsupported image file extension: " + extension); // Get the target directory. File targetDir; if (qualifier != null) { this.logger.fine( "Request: set template image: " + name + '/' + qualifier + "." ); final ApplicationTemplate template = this.manager.applicationTemplateMngr().findTemplate(name, qualifier); if (template == null) throw new NoSuchElementException("Cannot find template: " + name + '/' + qualifier); targetDir = new File(template.getDirectory(), Constants.PROJECT_DIR_DESC); } else { this.logger.fine( "Request: set application image: " + name + "." ); final Application application = this.manager.applicationMngr().findApplicationByName( name ); if (application == null) throw new NoSuchElementException("Cannot find application: " + name); targetDir = new File(application.getDirectory(), Constants.PROJECT_DIR_DESC); } // First clean the previous "application.*" images, as they may be chosen instead of the one we're uploading. for (final String ext : SUPPORTED_EXTENSIONS) { File f = new File(targetDir, "application." + ext); Utils.deleteFilesRecursivelyAndQuietly( f ); } // Now store the image: rename it to application.X, so we get sure it is chosen as THE app/template icon. // (where X is the uploaded file extension). Utils.copyStream(image, new File(targetDir, "application." + extension)); } /** * Get the file name extension from its name. * @param filename the filename. * @return the file name extension. */ private static String getFileExtension(final String filename) { String extension = ""; int i = filename.lastIndexOf('.'); int p = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); if (i > p) extension = filename.substring(i+1); return extension; } }