/*
###############################################################################
# #
# 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;
}
}