/* ############################################################################### # # # Copyright (C) 2011-2016 OpenMEAP, Inc. # # Credits to Jonathan Schang & Rob Thacher # # # # Released under the LGPLv3 # # # # OpenMEAP is free software: you can redistribute it and/or modify # # it under the terms of the GNU Lesser General Public License as published # # by the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # OpenMEAP is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU Lesser General Public License for more details. # # # # You should have received a copy of the GNU Lesser General Public License # # along with OpenMEAP. If not, see <http://www.gnu.org/licenses/>. # # # ############################################################################### */ package com.openmeap.admin.web.backing; import static com.openmeap.util.ParameterMapUtils.firstValue; import static com.openmeap.util.ParameterMapUtils.notEmpty; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.zip.ZipFile; import javax.persistence.PersistenceException; import org.apache.commons.fileupload.FileItem; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.openmeap.Authorizer; import com.openmeap.admin.web.events.AddSubNavAnchorEvent; import com.openmeap.constants.FormConstants; import com.openmeap.event.MessagesEvent; import com.openmeap.event.ProcessingEvent; import com.openmeap.event.ProcessingTargets; import com.openmeap.model.InvalidPropertiesException; import com.openmeap.model.ModelManager; import com.openmeap.model.ModelServiceOperation; import com.openmeap.model.dto.Application; import com.openmeap.model.dto.ApplicationArchive; import com.openmeap.model.dto.ApplicationVersion; import com.openmeap.model.dto.Deployment; import com.openmeap.model.dto.GlobalSettings; import com.openmeap.model.event.ModelEntityEvent; import com.openmeap.model.event.notifier.ArchiveFileUploadNotifier; import com.openmeap.protocol.dto.HashAlgorithm; import com.openmeap.util.ParameterMapUtils; import com.openmeap.util.ServletUtils; import com.openmeap.util.Utils; import com.openmeap.util.ZipUtils; import com.openmeap.web.AbstractTemplatedSectionBacking; import com.openmeap.web.GenericProcessingEvent; import com.openmeap.web.ProcessingContext; import com.openmeap.web.ProcessingUtils; import com.openmeap.web.html.Anchor; import com.openmeap.web.html.Option; // TODO: there are way to many things going on in this class public class AddModifyApplicationVersionBacking extends AbstractTemplatedSectionBacking { private Logger logger = LoggerFactory.getLogger(AddModifyApplicationVersionBacking.class); private static String PROCESS_TARGET = ProcessingTargets.ADDMODIFY_APPVER; private ModelManager modelManager = null; private ArchiveFileUploadNotifier archiveUploadNotifier = null; public AddModifyApplicationVersionBacking() { setProcessingTargetIds(Arrays.asList(new String[]{PROCESS_TARGET})); } /** * With the first of the bean name matching "addModifyApp", there are * three ways to access this: * - request has applicationId and processTarget - modifying an application * - request has applicationId only - pulling up an application to modify * - request has processTarget only - submitting a brand new application * * See the WEB-INF/ftl/form-application-addmodify.ftl for input/output parameters. * * @param context Not referenced at all, may be null * @param templateVariables Variables output to for the view * @param parameterMap Parameters passed in to drive processing * @return on errors, returns an array of error processingevents * @see TemplatedSectionBacking::process() */ public Collection<ProcessingEvent> process(ProcessingContext context, Map<Object,Object> templateVariables, Map<Object, Object> parameterMap) { List<ProcessingEvent> events = new ArrayList<ProcessingEvent>(); Application app = null; ApplicationVersion version = null; // make sure we're configured to accept uploads, warn otherwise validateStorageConfiguration(templateVariables,events); // we must have an application in order to add a version if( ! notEmpty(FormConstants.APP_ID, parameterMap) ) { return ProcessingUtils.newList(new GenericProcessingEvent<String>(ProcessingTargets.MESSAGES,"An application must be specified in order to add a version")); } Long appId = Long.valueOf( firstValue(FormConstants.APP_ID, parameterMap) ); app = modelManager.getModelService().findByPrimaryKey(Application.class, appId ); if( app==null ) { return ProcessingUtils.newList(new GenericProcessingEvent<String>(ProcessingTargets.MESSAGES,"The application with id "+appId+" could not be found.")); } events.add( new AddSubNavAnchorEvent(new Anchor("?bean=addModifyAppPage&applicationId="+app.getId(),"View/Modify Application","View/Modify Application")) ); events.add( new AddSubNavAnchorEvent(new Anchor("?bean=appVersionListingsPage&applicationId="+app.getId(),"Version Listings","Version Listings")) ); events.add( new AddSubNavAnchorEvent(new Anchor("?bean=deploymentListingsPage&applicationId="+app.getId(),"Deployment History","Deployment History")) ); // at this point, we're committed to form setup at least templateVariables.put(FormConstants.PROCESS_TARGET,PROCESS_TARGET); version = obtainExistingApplicationVersionFromParameters(app,appId,events,parameterMap); if( version==null ) { version = new ApplicationVersion(); } // determine if the user is allowed to modify application versions Boolean willProcess = canUserModifyOrCreate(app,version); if( !willProcess ) { events.add( new MessagesEvent("Current user does not have permissions to make changes here.") ); } if( !version.getActiveFlag() ) { events.add( new MessagesEvent("This version is not currently active.") ); willProcess=false; } templateVariables.put("willProcess",willProcess); if( notEmpty(FormConstants.PROCESS_TARGET, parameterMap) && PROCESS_TARGET.compareTo(firstValue(FormConstants.PROCESS_TARGET,parameterMap ))==0 && willProcess ) { // TODO: check to see if the user can delete versions if( ParameterMapUtils.notEmpty(FormConstants.DELETE,parameterMap) && ParameterMapUtils.notEmpty("deleteConfirm",parameterMap) ) { if( ParameterMapUtils.firstValue("deleteConfirm", parameterMap).equals(FormConstants.APPVER_DELETE_CONFIRM_TEXT) ) { try { modelManager.begin(); modelManager.delete(version, events); modelManager.commit(events); } catch(Exception e) { modelManager.rollback(); String msg = String.format("Unable to delete the version - %s",ExceptionUtils.getRootCauseMessage(e)); logger.error(msg,e); events.add( new MessagesEvent(msg) ); } } else { events.add( new MessagesEvent("You must confirm your desire to delete by typing in the delete confirmation message.") ); } } else { processApplicationVersionFromParameters(app,version,events,parameterMap); } } if( version!=null ) { templateVariables.put("version", version); } templateVariables.put("application", app); createHashTypes(templateVariables,version!=null?version.getArchive():null); return events; } private void validateStorageConfiguration(Map<Object,Object> templateVariables, List<ProcessingEvent> events) { String storagePathErrors = modelManager.getGlobalSettings().validateTemporaryStoragePath(); if( storagePathErrors!=null ) { events.add( new MessagesEvent("WARNING: The archive storage path is not set and file uploads will not be processed. The archive storage path can be set on the settings page.") ); templateVariables.put(FormConstants.ENCODING_TYPE, ""); } else { templateVariables.put(FormConstants.ENCODING_TYPE,"enctype=\""+FormConstants.ENCTYPE_MULTIPART_FORMDATA+"\""); } } private Boolean canUserModifyOrCreate(Application app, ApplicationVersion version) { // we don't want to pass it back, but the // Authorizer needs the Application object // to determine whether the user may create // a version or not. version = version!=null?version:new ApplicationVersion(); version.setApplication(app); Boolean mayCreateVersion = modelManager.getAuthorizer().may(Authorizer.Action.CREATE, version); Boolean mayModifyVersion = modelManager.getAuthorizer().may(Authorizer.Action.MODIFY, version); return (mayCreateVersion || (mayModifyVersion && version!=null)); } /** * Creates the list of selectable hashes * @param vars * @param archive */ @SuppressWarnings("unchecked") private void createHashTypes(Map<Object,Object> vars, ApplicationArchive archive) { List<Option> opts = new ArrayList<Option>(); String archiveHashAlg = archive!=null?archive.getHashAlgorithm():null; HashAlgorithm alg = null; for( HashAlgorithm thisAlg : HashAlgorithm.values() ) { Option newOpt = new Option(); newOpt.setIsSelected(archiveHashAlg!=null && thisAlg.value().equals(archiveHashAlg)); newOpt.setInnerText(thisAlg.value()); newOpt.setValue(thisAlg.value()); opts.add(newOpt); } vars.put("hashTypes", opts); } /** * @param app * @param appId * @param events * @param parameterMap * @return The application version indicated by the parameterMap, or null */ private ApplicationVersion obtainExistingApplicationVersionFromParameters(Application app, Long appId, List<ProcessingEvent> events, Map<Object,Object> parameterMap) { // if we're not processing and there is a versionId or an identifier in the request // then we're pre-populating the form with information from the version ApplicationVersion version = null; String versionId = firstValue("versionId", parameterMap); String identifier = firstValue("identifier", parameterMap); if( StringUtils.isNotBlank(versionId) || StringUtils.isNotBlank(identifier) ) { if( StringUtils.isNotBlank(versionId) ) { version = modelManager.getModelService().findByPrimaryKey(ApplicationVersion.class,Long.valueOf(versionId)); } if( version==null && StringUtils.isNotBlank(identifier) ) { version = modelManager.getModelService().findAppVersionByNameAndId(app.getName(), identifier); } if( version==null ) { events.add( new GenericProcessingEvent(ProcessingTargets.MESSAGES,"An Application Version matching input could not be found. Creating a new version.") ); } else if( version.getApplication()!=null && version.getApplication().getId().compareTo(appId)!=0 ){ version = null; events.add( new GenericProcessingEvent(ProcessingTargets.MESSAGES,"The Application Version with id "+versionId+" is not a version of the Application with id "+appId) ); } } return version; } private void processApplicationVersionFromParameters(Application app, ApplicationVersion version, List<ProcessingEvent> events, Map<Object,Object> parameterMap) { // a version is not being modified, // then create a new archive for it. if( version.getPk()==null ) { version.setArchive(new ApplicationArchive()); version.getArchive().setApplication(app); version.setApplication(app); } fillInApplicationVersionFromParameters(app,version,events,parameterMap); if( version!=null && version.getArchive()==null ) { events.add( new MessagesEvent("Application archive could not be created. Not creating empty version.") ); } else { try { modelManager.begin(); version.setLastModifier(firstValue("userPrincipalName",parameterMap)); ApplicationArchive savedArchive = version.getArchive(); version.setArchive(null); savedArchive = modelManager.addModify(savedArchive, events); version.setArchive(savedArchive); version = modelManager.addModify(version,events); app.addVersion(version); app = modelManager.addModify(app,events); modelManager.commit(events); modelManager.refresh(app,events); events.add( new MessagesEvent("Application version successfully created/modified!") ); } catch( InvalidPropertiesException ipe ) { modelManager.rollback(); logger.error("Unable to add/modify version "+version.getIdentifier(),ipe); events.add( new MessagesEvent("Unable to add/modify version - "+ipe.getMessage()) ); } catch( PersistenceException pe ) { modelManager.rollback(); logger.error("Unable to add/modify version "+version.getIdentifier(),pe); events.add( new MessagesEvent("Unable to add/modify version - "+pe.getMessage()) ); } } } private void fillInApplicationVersionFromParameters(Application app, ApplicationVersion version, List<ProcessingEvent> events, Map<Object,Object> parameterMap) { version.setIdentifier(firstValue("identifier",parameterMap)); if( version.getArchive()==null ) { version.setArchive(new ApplicationArchive()); version.getArchive().setApplication(app); } version.setApplication(app); version.setNotes(firstValue("notes",parameterMap)); Boolean archiveUncreated = true; // if there was an uploadArchive, then attempt to auto-assemble the rest of parameters if( parameterMap.get("uploadArchive")!=null ) { if( ! (parameterMap.get("uploadArchive") instanceof FileItem) ) { events.add( new MessagesEvent("Uploaded file not processed! Is the archive storage path set in settings?") ); } else { FileItem item = (FileItem)parameterMap.get("uploadArchive"); Long size = item.getSize(); if( size>0 ) { try { File tempFile = ServletUtils.tempFileFromFileItem(modelManager.getGlobalSettings().getTemporaryStoragePath(), item); ApplicationArchive archive = version.getArchive(); archive.setNewFileUploaded(tempFile.getAbsolutePath()); archiveUncreated = false; } catch(Exception ioe) { logger.error("An error transpired creating an uploadArchive temp file: {}",ioe); events.add( new MessagesEvent(ioe.getMessage()) ); return; } finally { item.delete(); } } else { events.add( new MessagesEvent("Uploaded file not processed! Is the archive storage path set in settings?") ); } } } // else there was no zip archive uploaded if( archiveUncreated ) { ApplicationArchive archive = version.getArchive(); archive.setHashAlgorithm(firstValue("hashType",parameterMap)); archive.setHash(firstValue("hash",parameterMap)); ApplicationArchive arch = modelManager.getModelService().findApplicationArchiveByHashAndAlgorithm(app, archive.getHash(), archive.getHashAlgorithm()); if(arch!=null) { version.setArchive(arch); archive = arch; } archive.setUrl(firstValue("url",parameterMap)); if( notEmpty("bytesLength",parameterMap) ) { archive.setBytesLength(Integer.valueOf(firstValue("bytesLength",parameterMap))); } if( notEmpty("bytesLengthUncompressed",parameterMap) ) { archive.setBytesLengthUncompressed(Integer.valueOf(firstValue("bytesLengthUncompressed",parameterMap))); } } } // ACCESSORS public void setModelManager(ModelManager modelManager) { this.modelManager = modelManager; } public ModelManager getModelManager() { return modelManager; } }