package org.springframework.ide.eclipse.boot.dash.dialogs;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.AnnotationModelEvent;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelListener;
import org.eclipse.jface.text.source.IAnnotationModelListenerExtension;
import org.eclipse.ui.editors.text.TextFileDocumentProvider;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.DocumentProviderRegistry;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.IElementStateListener;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ApplicationManifestHandler;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFApplication;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.AppNameAnnotation;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.AppNameAnnotationModel;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.deployment.CloudApplicationDeploymentProperties;
import org.springframework.ide.eclipse.boot.dash.model.AbstractDisposable;
import org.springframework.ide.eclipse.boot.dash.model.UserInteractions;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.editor.support.reconcile.ReconcileProblemAnnotation;
import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression;
import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable;
import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult;
import org.springsource.ide.eclipse.commons.livexp.core.Validator;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.Yaml;
public class DeploymentPropertiesDialogModel extends AbstractDisposable {
public static final String UNKNOWN_DEPLOYMENT_MANIFEST_TYPE_MUST_BE_EITHER_FILE_OR_MANUAL = "Unknown deployment manifest type. Must be either 'File' or 'Manual'.";
public static final String NO_SUPPORT_TO_DETERMINE_APP_NAMES = "Support for determining application names is unavailable";
public static final String MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME = "Manifest does not contain deployment properties for application with name ''{0}''.";
public static final String APPLICATION_NAME_NOT_SELECTED = "Application name not selected";
public static final String MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED = "Manifest does not have any application defined.";
public static final String ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY = "Enter deployment manifest YAML manually.";
public static final String CURRENT_GENERATED_DEPLOYMENT_MANIFEST = "Current generated deployment manifest.";
public static final String CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM = "Choose an existing deployment manifest YAML file from the local file system.";
public static final String DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED = "Deployment manifest file not selected.";
public static final String MANIFEST_YAML_ERRORS = "Deployment manifest YAML has errors.";
public static enum ManifestType {
FILE,
MANUAL
}
private UserInteractions ui;
private abstract class AbstractSubModel {
LiveVariable<AppNameAnnotationModel> appNameAnnotationModel = new LiveVariable<>();
LiveVariable<IAnnotationModel> resourceAnnotationModel = new LiveVariable<>();
LiveExpression<List<String>> applicationNames = new LiveExpression<List<String>>() {
private AppNameAnnotationModel attachedTo = null;
private AnnotationModelListener listener = new AnnotationModelListener() {
public void modelChanged(AnnotationModelEvent event) {
refresh();
}
};
{
dependsOn(appNameAnnotationModel);
}
@Override
protected List<String> compute() {
AppNameAnnotationModel annotationModel = appNameAnnotationModel.getValue();
attachListener(annotationModel);
if (annotationModel != null) {
List<String> applicationNames = new ArrayList<>();
for (Iterator<Annotation> itr = annotationModel.getAnnotationIterator(); itr.hasNext();) {
Annotation next = itr.next();
if (next instanceof AppNameAnnotation) {
AppNameAnnotation a = (AppNameAnnotation) next;
applicationNames.add(a.getText());
}
}
return applicationNames;
}
return Collections.emptyList();
}
synchronized private void attachListener(AppNameAnnotationModel annotationModel) {
if (attachedTo == annotationModel) {
return;
}
if (attachedTo != null) {
attachedTo.removeAnnotationModelListener(listener);
}
if (annotationModel != null) {
annotationModel.addAnnotationModelListener(listener);
}
attachedTo = annotationModel;
}
};
LiveExpression<Boolean> errorsInYaml = new LiveExpression<Boolean>() {
private IAnnotationModel attachedTo = null;
private AnnotationModelListener listener = new AnnotationModelListener() {
public void modelChanged(AnnotationModelEvent event) {
refresh();
}
};
{
dependsOn(resourceAnnotationModel);
}
{
onDispose((d) -> {
if (attachedTo != null) {
attachedTo.removeAnnotationModelListener(listener);
}
});
}
@Override
protected Boolean compute() {
IAnnotationModel annotationModel = resourceAnnotationModel.getValue();
attachListener(annotationModel);
if (annotationModel != null) {
for (Iterator<Annotation> itr = annotationModel.getAnnotationIterator(); itr.hasNext();) {
Annotation next = itr.next();
if (ReconcileProblemAnnotation.ERROR_ANNOTATION_TYPE == next.getType()) {
return Boolean.TRUE;
}
}
}
return Boolean.FALSE;
}
synchronized private void attachListener(IAnnotationModel annotationModel) {
if (attachedTo == annotationModel) {
return;
}
if (attachedTo != null) {
attachedTo.removeAnnotationModelListener(listener);
}
if (annotationModel != null) {
annotationModel.addAnnotationModelListener(listener);
}
attachedTo = annotationModel;
}
};
LiveExpression<String> selectedAppName = new LiveExpression<String>() {
private AppNameAnnotationModel attachedTo = null;
private AnnotationModelListener listener = new AnnotationModelListener() {
public void modelChanged(AnnotationModelEvent event) {
refresh();
}
};
{
dependsOn(appNameAnnotationModel);
}
{
onDispose((d) -> {
if (attachedTo != null) {
attachedTo.removeAnnotationModelListener(listener);
}
});
}
@Override
protected String compute() {
AppNameAnnotationModel annotationModel = appNameAnnotationModel.getValue();
attachListener(annotationModel);
if (annotationModel != null) {
AppNameAnnotation a = annotationModel.getSelectedAppAnnotation();
if (a != null) {
return a.getText();
}
}
return null;
}
synchronized private void attachListener(AppNameAnnotationModel annotationModel) {
if (attachedTo == annotationModel) {
return;
}
if (attachedTo != null) {
attachedTo.removeAnnotationModelListener(listener);
}
if (annotationModel != null) {
annotationModel.addAnnotationModelListener(listener);
}
attachedTo = annotationModel;
}
};
{
onDispose((d) -> {
applicationNames.dispose();
appNameAnnotationModel.dispose();
errorsInYaml.dispose();
resourceAnnotationModel.dispose();
selectedAppName.dispose();
});
}
abstract String getManifestContents();
/**
* Return manifest from which contents are takes as an {@link IFile}
* Return null if manifest content doesn't come from a file
* @return
*/
abstract IFile getManifest();
CloudApplicationDeploymentProperties getDeploymentProperties() throws Exception {
List<CloudApplicationDeploymentProperties> propsList = new ApplicationManifestHandler(project, cloudData, getManifest()) {
@Override
protected InputStream getInputStream() throws Exception {
return new ByteArrayInputStream(getManifestContents().getBytes());
}
}.load(new NullProgressMonitor());
/*
* If "Select Manifest..." action is invoked appName is not null,
* but we should allow for any manifest file selected for now. Hence
* set the applicationName var to null in that case
*/
CloudApplicationDeploymentProperties deploymentProperties = null;
String applicationName = deployedApp == null ? selectedAppName.getValue() : getDeployedAppName();
if (applicationName == null) {
deploymentProperties = propsList.get(0);
} else {
for (CloudApplicationDeploymentProperties p : propsList) {
if (applicationName.equals(p.getAppName())) {
deploymentProperties = p;
break;
}
}
}
return deploymentProperties;
}
}
public class FileDeploymentPropertiesDialogModel extends AbstractSubModel {
private boolean inputConnected = false;
private final Set<TextFileDocumentProvider> docProviders = new HashSet<>();
private final LiveVariable<IResource> selectedFile = new LiveVariable<>();
private final LiveExpression<FileEditorInput> editorInput = new LiveExpression<FileEditorInput>() {
{
dependsOn(selectedFile);
}
@Override
protected FileEditorInput compute() {
IFile file = getFile();
FileEditorInput currentInput = getValue();
boolean changed = currentInput == null || !currentInput.getFile().equals(file);
if (changed) {
saveOrDiscardIfNeeded(currentInput);
if (file != null) {
FileEditorInput input = new FileEditorInput(file);
// Connect input to doc provider here when editor input has changed
TextFileDocumentProvider provider = getTextDocumentProvider(input);
if (provider != null) {
connect(provider, input);
}
return input;
}
}
return null;
}
};
private final LiveExpression<IDocument> document = new LiveExpression<IDocument>(new Document("")) {
{
dependsOn(editorInput);
}
@Override
protected IDocument compute() {
FileEditorInput input = editorInput.getValue();
if (input != null) {
TextFileDocumentProvider provider = getTextDocumentProvider(input);
if (provider != null) {
return provider.getDocument(input);
}
}
return new Document("");
}
};
final private LiveExpression<String> fileLabel = new LiveExpression<String>() {
{
dependsOn(editorInput);
}
@Override
protected String compute() {
FileEditorInput input = editorInput.getValue();
if (input != null) {
boolean dirty = getTextDocumentProvider(input).canSaveDocument(input);
return editorInput.getValue().getFile().getFullPath().toOSString() + (dirty ? "*" : "");
}
return "";
}
};
Validator validator = new Validator() {
{
dependsOn(editorInput);
dependsOn(appNameAnnotationModel);
dependsOn(errorsInYaml);
dependsOn(applicationNames);
dependsOn(selectedAppName);
}
@Override
protected ValidationResult compute() {
ValidationResult result = ValidationResult.OK;
if (editorInput.getValue() == null) {
result = ValidationResult.error(DEPLOYMENT_MANIFEST_FILE_NOT_SELECTED);
}
if (result.isOk()) {
AppNameAnnotationModel appNamesModel = appNameAnnotationModel.getValue();
if (appNamesModel == null) {
result = ValidationResult.error(NO_SUPPORT_TO_DETERMINE_APP_NAMES);
}
if (result.isOk()) {
String appName = getDeployedAppName();
if (applicationNames.getValue().isEmpty()) {
result = ValidationResult.error(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED);
} else {
if (errorsInYaml.getValue().booleanValue()) {
result = ValidationResult.error(MANIFEST_YAML_ERRORS);
} else {
String selectedAnnotation = selectedAppName.getValue();
if (appName == null) {
if (selectedAnnotation == null) {
result = ValidationResult.error(APPLICATION_NAME_NOT_SELECTED);
}
} else {
if (selectedAnnotation == null || !appName.equals(selectedAnnotation)) {
result = ValidationResult.error(MessageFormat.format(
MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME,
appName));
}
}
}
}
}
}
if (result.isOk()) {
result = ValidationResult.info(CHOOSE_AN_EXISTING_DEPLOYMENT_MANIFEST_YAML_FILE_FROM_THE_LOCAL_FILE_SYSTEM);
}
return result;
}
};
private IElementStateListener dirtyStateListener = new IElementStateListener() {
@Override
public void elementMoved(Object arg0, Object arg1) {
}
@Override
public void elementDirtyStateChanged(final Object file, final boolean dirty) {
FileEditorInput editorInputValue = editorInput.getValue();
if (editorInputValue != null && editorInputValue.equals(file)) {
fileLabel.refresh();
}
}
@Override
public void elementDeleted(Object arg0) {
}
@Override
public void elementContentReplaced(Object file) {
if (file.equals(editorInput.getValue())) {
document.refresh();
}
}
@Override
public void elementContentAboutToBeReplaced(Object arg0) {
}
};
{
onDispose((d) -> {
saveOrDiscardIfNeeded();
validator.dispose();
document.dispose();
editorInput.dispose();
fileLabel.dispose();
selectedFile.dispose();
});
}
private void saveOrDiscardIfNeeded() {
FileEditorInput input = editorInput.getValue();
if (input != null) {
saveOrDiscardIfNeeded(input);
}
}
private void saveOrDiscardIfNeeded(FileEditorInput file) {
TextFileDocumentProvider docProvider = file == null ? null : getTextDocumentProvider(file);
if (docProvider != null && file != null && file.exists() && inputConnected) {
if (docProvider.canSaveDocument(file) && ui.confirmOperation("Changes Detected", "Manifest file '" + file.getFile().getFullPath().toOSString()
+ "' has been changed. Do you want to save changes or discard them?", new String[] {"Save", "Discard"}, 0) == 0) {
try {
docProvider.saveDocument(new NullProgressMonitor(), file, docProvider.getDocument(file), true);
} catch (CoreException e) {
Log.log(e);
ui.errorPopup("Failed Saving File", ExceptionUtil.getMessage(e));
}
} else {
try {
docProvider.resetDocument(file);
} catch (CoreException e) {
Log.log(e);
}
}
disconnect(docProvider, file);
}
}
private TextFileDocumentProvider getTextDocumentProvider(FileEditorInput input) {
IDocumentProvider docProvider = DocumentProviderRegistry.getDefault().getDocumentProvider(input);
if (docProvider instanceof TextFileDocumentProvider) {
TextFileDocumentProvider textDocProvider = (TextFileDocumentProvider) docProvider;
if (!docProviders.contains(textDocProvider)) {
textDocProvider.addElementStateListener(dirtyStateListener);
onDispose((d) -> {
textDocProvider.removeElementStateListener(dirtyStateListener);
});
docProviders.add(textDocProvider);
}
return textDocProvider;
}
return null;
}
public IAnnotationModel getAnnotationModel() {
FileEditorInput input = editorInput.getValue();
if (input != null) {
TextFileDocumentProvider provider = getTextDocumentProvider(input);
if (provider != null) {
return provider.getAnnotationModel(input);
}
}
return null;
}
@Override
String getManifestContents() {
return document.getValue().get();
}
private IFile getFile() {
IResource r = selectedFile.getValue();
return r instanceof IFile ? (IFile) r : null;
}
@Override
IFile getManifest() {
return getFile();
}
private void connect(TextFileDocumentProvider provider, FileEditorInput input) {
if (!inputConnected) {
try {
provider.connect(input);
inputConnected = true;
} catch (CoreException e) {
Log.log(e);
}
} else {
throw new IllegalStateException("Attempting to connect input '" + input.getFile().getName() + "' while previous input is still connected.");
}
}
private void disconnect(TextFileDocumentProvider provider, FileEditorInput input) {
if (inputConnected) {
provider.disconnect(input);
inputConnected = false;
} else {
throw new IllegalStateException("Attempting to disconnect input '" + input.getFile().getName() + "' while no inputs are connected");
}
}
void reopenSameFile() {
try {
FileEditorInput input = editorInput.getValue();
if (input != null) {
TextFileDocumentProvider provider = getTextDocumentProvider(input);
if (provider != null) {
connect(provider, input);
}
}
// Update the document such that it's the document coming from the doc provider
document.refresh();
} catch (Exception e) {
Log.log(e);
}
}
}
public class ManualDeploymentPropertiesDialogModel extends AbstractSubModel {
private IDocument document;
private boolean readOnly;
private IAnnotationModel annotationModel = new AnnotationModel();
Validator validator = new Validator() {
{
dependsOn(appNameAnnotationModel);
dependsOn(errorsInYaml);
dependsOn(applicationNames);
dependsOn(selectedAppName);
}
@Override
protected ValidationResult compute() {
ValidationResult result = ValidationResult.OK;
AppNameAnnotationModel appNamesModel = appNameAnnotationModel.getValue();
if (appNamesModel == null) {
result = ValidationResult.error(NO_SUPPORT_TO_DETERMINE_APP_NAMES);
}
if (result.isOk()) {
String appName = getDeployedAppName();
if (applicationNames.getValue().isEmpty()) {
result = ValidationResult.error(MANIFEST_DOES_NOT_HAVE_ANY_APPLICATION_DEFINED);
} else {
if (errorsInYaml.getValue().booleanValue()) {
result = ValidationResult.error(MANIFEST_YAML_ERRORS);
} else {
String selectedAnnotation = selectedAppName.getValue();
if (appName == null) {
if (selectedAnnotation == null) {
result = ValidationResult.error(APPLICATION_NAME_NOT_SELECTED);
}
} else {
if (selectedAnnotation == null || !appName.equals(selectedAnnotation)) {
result = ValidationResult.error(MessageFormat.format(
MANIFEST_DOES_NOT_CONTAIN_DEPLOYMENT_PROPERTIES_FOR_APPLICATION_WITH_NAME,
appName));
}
}
}
}
}
if (result.isOk()) {
result = ValidationResult.info(readOnly ? CURRENT_GENERATED_DEPLOYMENT_MANIFEST : ENTER_DEPLOYMENT_MANIFEST_YAML_MANUALLY);
}
return result;
}
};
{
onDispose((d) -> {
validator.dispose();
});
}
ManualDeploymentPropertiesDialogModel(boolean readOnly) {
super();
this.readOnly = readOnly;
this.document = new Document(generateDefaultContent());
}
public void setText(String s) {
if (readOnly) {
throw new IllegalStateException("The model is read-only!");
}
document.set(s);
}
public String getText() {
return document.get();
}
public IAnnotationModel getAnnotationModel() {
return annotationModel;
}
@Override
String getManifestContents() {
return getText();
}
@Override
IFile getManifest() {
return null;
}
private String generateDefaultContent() {
CloudApplicationDeploymentProperties props = CloudApplicationDeploymentProperties.getFor(project, cloudData,
getDeployedApp());
Map<Object, Object> yaml = ApplicationManifestHandler.toYaml(props, cloudData);
DumperOptions options = new DumperOptions();
options.setExplicitStart(true);
options.setCanonical(false);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(FlowStyle.BLOCK);
return new Yaml(options).dump(yaml);
}
}
private abstract class AnnotationModelListener implements IAnnotationModelListener, IAnnotationModelListenerExtension {
@Override
public void modelChanged(IAnnotationModel model) {
// Leave empty. AnnotationModelEvent method is the one that will be called
}
@Override
abstract public void modelChanged(AnnotationModelEvent event);
}
final public LiveVariable<ManifestType> type = new LiveVariable<>();
final private CFApplication deployedApp;
final private Map<String, Object> cloudData;
final IProject project;
final private FileDeploymentPropertiesDialogModel fileModel;
final private ManualDeploymentPropertiesDialogModel manualModel;
private boolean isCancelled = false;
final private Validator validator;
public DeploymentPropertiesDialogModel(UserInteractions ui, Map<String, Object> cloudData, IProject project, CFApplication deployedApp) {
super();
this.ui = ui;
this.deployedApp = deployedApp;
this.cloudData = cloudData;
this.project = project;
this.manualModel = new ManualDeploymentPropertiesDialogModel(deployedApp != null);
this.fileModel = new FileDeploymentPropertiesDialogModel();
this.validator = new Validator() {
{
dependsOn(type);
dependsOn(fileModel.validator);
dependsOn(manualModel.validator);
}
@Override
protected ValidationResult compute() {
if (isFileManifestType()) {
return fileModel.validator.getValue();
} else if (isManualManifestType()) {
return manualModel.validator.getValue();
} else {
return ValidationResult.error(UNKNOWN_DEPLOYMENT_MANIFEST_TYPE_MUST_BE_EITHER_FILE_OR_MANUAL);
}
}
};
}
public CloudApplicationDeploymentProperties getDeploymentProperties() throws Exception {
if (isCancelled) {
throw new OperationCanceledException();
}
if (type.getValue() == null) {
return null;
}
switch (type.getValue()) {
case FILE:
return fileModel.getDeploymentProperties();
case MANUAL:
return manualModel.getDeploymentProperties();
default:
return null;
}
}
public void cancelPressed() {
fileModel.saveOrDiscardIfNeeded();
isCancelled = true;
}
public boolean okPressed() {
fileModel.saveOrDiscardIfNeeded();
isCancelled = false;
try {
return getDeploymentProperties()!=null;
} catch (Exception e) {
fileModel.reopenSameFile();
ui.errorPopup("Invalid YAML content", ExceptionUtil.getMessage(e));
return false;
}
}
public void setSelectedManifest(IResource manifest) {
fileModel.selectedFile.setValue(manifest);
}
public void setManualManifest(String manifestText) {
manualModel.setText(manifestText);
}
public void setManifestType(ManifestType type) {
this.type.setValue(type);
}
public LiveVariable<IResource> getSelectedManifestVar() {
return fileModel.selectedFile;
}
public String getProjectName() {
return project.getName();
}
public boolean isFileManifestType() {
return type.getValue() == ManifestType.FILE;
}
public boolean isManualManifestType() {
return type.getValue() == ManifestType.MANUAL;
}
public IResource getSelectedManifest() {
return getSelectedManifestVar().getValue();
}
public String getDeployedAppName() {
return deployedApp == null ? null : deployedApp.getName();
}
public IDocument getManualDocument() {
return manualModel.document;
}
public IAnnotationModel getManualAnnotationModel() {
return manualModel.annotationModel;
}
public boolean isManualManifestReadOnly() {
return deployedApp!=null;
}
public LiveExpression<IDocument> getFileDocument() {
return fileModel.document;
}
public IAnnotationModel getFileAnnotationModel() {
return fileModel.getAnnotationModel();
}
public LiveExpression<String> getFileLabel() {
return fileModel.fileLabel;
}
private CFApplication getDeployedApp() {
return deployedApp;
}
public void setFileAppNameAnnotationModel(AppNameAnnotationModel appNameAnnotationModel) {
fileModel.appNameAnnotationModel.setValue(appNameAnnotationModel);
}
public void setManualAppNameAnnotationModel(AppNameAnnotationModel appNameAnnotationModel) {
manualModel.appNameAnnotationModel.setValue(appNameAnnotationModel);
}
public boolean isCanceled() {
return isCancelled;
}
public Validator getValidator() {
return validator;
}
public LiveExpression<AppNameAnnotationModel> getManualAppNameAnnotationModel() {
return manualModel.appNameAnnotationModel;
}
public LiveExpression<AppNameAnnotationModel> getFileAppNameAnnotationModel() {
return fileModel.appNameAnnotationModel;
}
public void setFileResourceAnnotationModel(IAnnotationModel annotationModel) {
fileModel.resourceAnnotationModel.setValue(annotationModel);
}
public void setManualResourceAnnotationModel(IAnnotationModel annotationModel) {
manualModel.resourceAnnotationModel.setValue(annotationModel);
}
public IAnnotationModel getManualResourceAnnotationModel() {
return manualModel.resourceAnnotationModel.getValue();
}
public IAnnotationModel getFileResourceAnnotationModel() {
return fileModel.resourceAnnotationModel.getValue();
}
}