package bndtools.wizards.workspace;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.bndtools.templating.Resource;
import org.bndtools.templating.ResourceMap;
import org.bndtools.templating.Template;
import org.bndtools.utils.jface.ItalicStyler;
import org.bndtools.utils.swt.SWTConcurrencyUtil;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.wizard.IWizardContainer;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import aQute.lib.io.IO;
import bndtools.Plugin;
import bndtools.utils.ModificationLock;
public class WorkspacePreviewPage extends WizardPage {
private final Object lock = new Object();
private final ModificationLock modifyLock = new ModificationLock();
private final Set<String> existingFiles = new HashSet<>();
private final Map<String,String> resourceErrors = new HashMap<>();
private final Set<String> checkedPaths = new HashSet<>();
private static final String MSG_NOTHING_SELECTED = "Select an entry to view details";
private File targetDir;
private Template template;
private ResourceMap templateOutputs = null;
private String errorMessage = null;
private boolean seen = false;
private Table tblOutputs;
private CheckboxTableViewer vwrOutputs;
private Image imgAdded;
private Image imgOverwrite;
private Image imgError;
private final IRunnableWithProgress calculatePreviewTask = new IRunnableWithProgress() {
@Override
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
synchronized (lock) {
try {
if (templateOutputs == null) {
errorMessage = null;
existingFiles.clear();
resourceErrors.clear();
templateOutputs = template.generateOutputs(Collections.<String, List<Object>> emptyMap(), monitor);
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
for (Entry<String,Resource> entry : templateOutputs.entries()) {
// Check for existing files
File file = new File(targetDir, entry.getKey());
switch (entry.getValue().getType()) {
case Folder :
if (file.exists() && !file.isDirectory())
resourceErrors.put(entry.getKey(), String.format("Path already exists and is not a directory: %s", entry.getKey()));
break;
case File :
if (file.exists() && !isEqualContent(file, entry.getValue().getContent())) {
existingFiles.add(entry.getKey());
if (!file.isFile())
resourceErrors.put(entry.getKey(), String.format("Path already exists and is not a plain file: %s", entry.getKey()));
}
break;
default :
// ignore
}
// If the base folder has the same name as a project, but the project has a different location in the filesystem, then we
// can't import it.
String projectFolderName;
int slashIndex = entry.getKey().indexOf('/');
if (slashIndex < 0)
projectFolderName = entry.getKey();
else
projectFolderName = entry.getKey().substring(0, slashIndex);
File projectFolder = new File(targetDir, projectFolderName);
IProject project = workspaceRoot.getProject(projectFolderName);
if (project.exists()) {
File existingProjectLoc = project.getLocation().toFile();
if (!existingProjectLoc.equals(projectFolder))
resourceErrors.put(entry.getKey(), String.format("Cannot import project directory %s as it clashes with an existing project '%s' at location %s", projectFolder.getAbsolutePath(), project.getName(),
existingProjectLoc.getAbsolutePath()));
}
}
}
} catch (Exception e) {
errorMessage = e.getMessage();
}
}
SWTConcurrencyUtil.execForControl(tblOutputs, true, updateDisplayTask);
}
};
private final Runnable updateDisplayTask = new Runnable() {
@Override
public void run() {
synchronized (lock) {
if (errorMessage != null) {
setErrorMessage(errorMessage);
} else if (!resourceErrors.isEmpty()) {
setErrorMessage("Cannot expand the template due to errors on some resources.");
} else {
setErrorMessage(null);
if (!existingFiles.isEmpty()) {
setMessage("Some files will be overwritten", WARNING);
} else {
setMessage(null, WARNING);
}
}
List<String> viewerInput;
if (templateOutputs != null) {
viewerInput = new ArrayList<>(templateOutputs.entries().size());
for (Entry<String,Resource> entry : templateOutputs.entries())
viewerInput.add(entry.getKey());
seen = true;
} else {
viewerInput = Collections.emptyList();
seen = false;
}
checkedPaths.addAll(viewerInput);
if (tblOutputs != null && !tblOutputs.isDisposed() && vwrOutputs != null) {
vwrOutputs.setInput(viewerInput);
vwrOutputs.setAllChecked(true);
}
IWizardContainer container = getContainer();
if (container != null)
container.updateButtons();
}
}
};
public WorkspacePreviewPage() {
super("preview");
}
@Override
public void createControl(Composite parent) {
setTitle("Preview Changes");
setImageDescriptor(Plugin.imageDescriptorFromPlugin("icons/bndtools-wizban.png")); //$NON-NLS-1$
imgAdded = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "icons/incoming.gif").createImage(parent.getDisplay());
imgOverwrite = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "icons/conflict.gif").createImage(parent.getDisplay());
imgError = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "icons/error_obj.gif").createImage(parent.getDisplay());
int columns = 4;
Composite composite = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(columns, false);
composite.setLayout(layout);
setControl(composite);
Label lblTitle = new Label(composite, SWT.NONE);
lblTitle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, columns, 1));
// Table
tblOutputs = new Table(composite, SWT.BORDER | SWT.CHECK);
tblOutputs.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, columns, 1));
vwrOutputs = new CheckboxTableViewer(tblOutputs);
vwrOutputs.setContentProvider(ArrayContentProvider.getInstance());
vwrOutputs.setLabelProvider(new StyledCellLabelProvider() {
@Override
public void update(ViewerCell cell) {
StyledString label;
Image icon;
String path = (String) cell.getElement();
String error = resourceErrors.get(path);
if (error != null) {
label = new StyledString(path, ItalicStyler.INSTANCE_ERROR);
icon = imgError;
} else {
label = new StyledString(path);
icon = existingFiles.contains(path) ? imgOverwrite : imgAdded;
}
cell.setText(path);
cell.setStyleRanges(label.getStyleRanges());
cell.setImage(icon);
}
});
vwrOutputs.setSorter(new ViewerSorter(Collator.getInstance()));
// Details display
final Label lblDetails = new Label(composite, SWT.NONE);
lblDetails.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, columns, 1));
lblDetails.setText(MSG_NOTHING_SELECTED);
// Button Panel
Label spacer1 = new Label(composite, SWT.NONE);
spacer1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
Button btnSelectNonConflict = new Button(composite, SWT.PUSH);
btnSelectNonConflict.setText("Select Non-Conflicting");
btnSelectNonConflict.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
modifyLock.modifyOperation(new Runnable() {
@Override
public void run() {
checkedPaths.clear();
for (Entry<String,Resource> entry : templateOutputs.entries()) {
String path = entry.getKey();
if (!existingFiles.contains(path))
checkedPaths.add(path);
}
vwrOutputs.setCheckedElements(checkedPaths.toArray());
}
});
}
});
Button btnSelectAll = new Button(composite, SWT.PUSH);
btnSelectAll.setText("Select All");
btnSelectAll.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
vwrOutputs.setAllChecked(true);
}
});
Button btnSelectNone = new Button(composite, SWT.PUSH);
btnSelectNone.setText("Select None");
btnSelectNone.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
vwrOutputs.setAllChecked(false);
}
});
// Listeners
vwrOutputs.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
IStructuredSelection sel = (IStructuredSelection) vwrOutputs.getSelection();
if (sel.isEmpty()) {
lblDetails.setText(MSG_NOTHING_SELECTED);
} else {
String path = (String) sel.getFirstElement();
String resourceError = resourceErrors.get(path);
if (resourceError != null) {
lblDetails.setText(resourceError);
} else if (existingFiles.contains(path)) {
lblDetails.setText("This file already exists and will be overwritten");
} else {
lblDetails.setText("This file will be created");
}
}
}
});
vwrOutputs.addCheckStateListener(new ICheckStateListener() {
@Override
public void checkStateChanged(final CheckStateChangedEvent event) {
modifyLock.ifNotModifying(new Runnable() {
@Override
public void run() {
final String updatedPath = (String) event.getElement();
if (event.getChecked()) {
checkedPaths.add(updatedPath);
// Check any directories that are parents of this path
modifyLock.modifyOperation(new Runnable() {
@Override
public void run() {
for (Entry<String,Resource> entry : templateOutputs.entries()) {
String path = entry.getKey();
if (path.endsWith("/") && updatedPath.startsWith(path)) {
checkedPaths.add(path);
vwrOutputs.setChecked(path, true);
}
}
}
});
} else {
checkedPaths.remove(updatedPath);
// Uncheck any paths that are descendants of this path
if (updatedPath.endsWith("/")) {
modifyLock.modifyOperation(new Runnable() {
@Override
public void run() {
for (Entry<String,Resource> entry : templateOutputs.entries()) {
String path = entry.getKey();
if (path.startsWith(updatedPath)) {
checkedPaths.remove(path);
vwrOutputs.setChecked(path, false);
}
}
}
});
}
}
}
});
}
});
}
@Override
public void dispose() {
super.dispose();
imgAdded.dispose();
imgOverwrite.dispose();
imgError.dispose();
}
@Override
public boolean isPageComplete() {
return super.isPageComplete() && seen && errorMessage == null && resourceErrors.isEmpty();
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible) {
try {
getContainer().run(true, true, calculatePreviewTask);
} catch (InvocationTargetException e) {
errorMessage = e.getTargetException().getMessage();
} catch (InterruptedException e) {
errorMessage = e.getMessage();
}
updateDisplayTask.run();
}
}
public void setTemplate(Template template) {
// set the template and remove the resource lock
synchronized (lock) {
this.template = template;
this.templateOutputs = null;
}
updateDisplayTask.run();
}
public void setTargetDir(File file) {
synchronized (lock) {
this.targetDir = file;
this.templateOutputs = null;
}
updateDisplayTask.run();
}
public Template getTemplate() {
return template;
}
public ResourceMap getTemplateOutputs() {
synchronized (lock) {
return templateOutputs;
}
}
public File getTargetDir() {
return targetDir;
}
public Set<String> getCheckedPaths() {
return Collections.unmodifiableSet(checkedPaths);
}
private boolean isEqualContent(File file, InputStream content) throws IOException {
byte[] a = IO.read(file);
byte[] b = IO.read(content);
return Arrays.equals(a, b);
}
}