ContributedResourcesPreferencePage.java

/*
 * <copyright>
 * Copyright  2009 by Carnegie Mellon University, all rights reserved.
 *
 * Use of the Open Source AADL Tool Environment (OSATE) is subject to the terms of the license set forth
 * at http://www.eclipse.org/legal/epl-v10.html.
 *
 * NO WARRANTY
 *
 * ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL PROPERTY OR OTHER PROPERTY OR RIGHTS GRANTED OR PROVIDED BY
 * CARNEGIE MELLON UNIVERSITY PURSUANT TO THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON AN "AS-IS" BASIS.
 * CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED AS TO ANY MATTER INCLUDING,
 * BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, INFORMATIONAL CONTENT,
 * NONINFRINGEMENT, OR ERROR-FREE OPERATION. CARNEGIE MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT, SPECIAL OR
 * CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS OR INABILITY TO USE SAID INTELLECTUAL PROPERTY, UNDER THIS LICENSE,
 * REGARDLESS OF WHETHER SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH DAMAGES. LICENSEE AGREES THAT IT WILL NOT
 * MAKE ANY WARRANTY ON BEHALF OF CARNEGIE MELLON UNIVERSITY, EXPRESS OR IMPLIED, TO ANY PERSON CONCERNING THE
 * APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH THE DELIVERABLES UNDER THIS LICENSE.
 *
 * Licensee hereby agrees to defend, indemnify, and hold harmless Carnegie Mellon University, its trustees, officers,
 * employees, and agents from all claims or demands made against them (and any related losses, expenses, or
 * attorney's fees) arising out of, or relating to Licensee's and/or its sub licensees' negligent use or willful
 * misuse of or negligent conduct or willful misconduct regarding the Software, facilities, or other rights or
 * assistance granted by Carnegie Mellon University under this License, including, but not limited to, any claims of
 * product liability, personal injury, death, damage to property, or violation of any laws or regulations.
 *
 * Carnegie Mellon University Software Engineering Institute authored documents are sponsored by the U.S. Department
 * of Defense under Contract F19628-00-C-0003. Carnegie Mellon University retains copyrights in all material produced
 * under this contract. The U.S. Government retains a non-exclusive, royalty-free license to publish or reproduce these
 * documents, or allow others to do so, for U.S. Government purposes only pursuant to the copyright license
 * under the contract clause at 252.227.7013.
 *
 * </copyright>
 */
package org.osate.internal.ui.preferences;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.util.URI;
import org.eclipse.jface.layout.TreeColumnLayout;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
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.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
import org.eclipse.ui.model.WorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.osate.pluginsupport.PluginSupportPlugin;
import org.osate.pluginsupport.PluginSupportUtil;
import org.osate.pluginsupport.PredeclaredProperties;
import org.osate.ui.OsateUiPlugin;

/**
 * This class represents the OSATE > Instantiation workspace preferences.
 * @since 5.0
 */
public final class ContributedResourcesPreferencePage extends PreferencePage
		implements IWorkbenchPreferencePage {
	private Map<URI, URI> originalOverriddenAadl;
	private final Map<URI, URI> overriddenAadl = new HashMap<>();
	private List<URI> disabledContribResources = new ArrayList<URI>();

	private TreeViewer tree;
	private Button overrideButton;
	private Button restoreButton;
	private TreeNode selectedNode;
	private Label uriLabel;

	public ContributedResourcesPreferencePage() {
		super("Contributed Resources");
	}

	/**
	 * Create the field editors.
	 */
	@Override
	public Control createContents(final Composite parent) {
		// Save original settings, and initialize the local copy
		originalOverriddenAadl = PredeclaredProperties.getOverriddenResources();
		overriddenAadl.putAll(originalOverriddenAadl);

		final Composite composite = new Composite(parent, SWT.NONE);
		composite.setLayout(new GridLayout(2, true));

		Label disabledContribLabel = new Label(composite, SWT.NONE);
		disabledContribLabel.setLayoutData(new GridData(2, SWT.FILL, true, true));
		disabledContribLabel.setText(
				"All property sets and packages contributed by plug-ins are added to each build by default. To exclude unnecessary contributions, set a checkbox below to checked");

		SashForm sashForm = new SashForm(composite, SWT.HORIZONTAL);
		GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1);
		sashForm.setLayoutData(gd);
		initializeDialogUnits(sashForm);

		tree = createTree(sashForm);
		tree.setLabelProvider(new FileLabelProvider(null, null));
		tree.setContentProvider(new TreeContentProvider());
		tree.setAutoExpandLevel(3);

		tree.setInput(createTreeHierarchy());
		tree.setComparator(new ViewerComparator());

		Sorter sort = new Sorter();
		tree.setSorter(sort);

		setCheckBoxesDisabledContributions();

		tree.addSelectionChangedListener(event -> {
			IStructuredSelection selection = (IStructuredSelection) event.getSelection();

			if (selection.isEmpty()) {
				restoreButton.setEnabled(false);
				uriLabel.setText("");
			}

			for (final Iterator<?> iter = selection.iterator(); iter.hasNext();) {
				final Object object = iter.next();
				if (object instanceof TreeNode) {
					selectedNode = (TreeNode) object;
					restoreButton.setEnabled(selectedNode.overridden);
					overrideButton.setEnabled(selectedNode.canOverride());
					uriLabel.setText(selectedNode.path);
				}
			}
		});
		tree.addDoubleClickListener(event -> {
			final TreeViewer viewer = (TreeViewer) event.getViewer();
			final IStructuredSelection thisSelection = (IStructuredSelection) event.getSelection();
			final TreeNode selectedNode = (TreeNode) thisSelection.getFirstElement();
			if (selectedNode != null) {
				if (selectedNode.getNode().isEmpty()) {
					doOverrideAction(selectedNode);
				} else {
					viewer.setExpandedState(selectedNode, !viewer.getExpandedState(selectedNode));
				}
			}
		});

		overrideButton = new Button(composite, SWT.PUSH);
		overrideButton.setLayoutData(new GridData(SWT.RIGHT, SWT.FILL, false, false));
		overrideButton.setText("Override");
		overrideButton.setToolTipText("Override the URI of the contributed resource with a URI from the workspace.");
		overrideButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(final SelectionEvent e) {
				if (selectedNode != null) {
					doOverrideAction(selectedNode);
				}
			}
		});

		restoreButton = new Button(composite, SWT.PUSH);
		restoreButton.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, false));
		restoreButton.setText("Restore");
		restoreButton.setToolTipText("Restore contributed resource to its plugin-specified URI.");
		restoreButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(final SelectionEvent e) {
				if (selectedNode != null) {
					URI uri = URI.createURI(selectedNode.path);
					if (uri != null) {
						overriddenAadl.remove(uri);
						restoreButton.setEnabled(false);

						if (disabledContribResources != null) {
							disabledContribResources.remove(uri); // remove override from disabled list as well
						}

						selectedNode.overridden = false;
						tree.refresh();
						uriLabel.setText(uriToReadable(uri));
					}
				}
			}
		});

		final Group labelGroup = new Group(composite, SWT.SHADOW_ETCHED_IN);
		labelGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
		labelGroup.setLayout(new GridLayout(1, true));

		labelGroup.setText("Contributed URI");
		uriLabel = new Label(labelGroup, SWT.NONE);
		uriLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		uriLabel.setText("");

		return composite;
	}

	private void setCheckBoxesDisabledContributions() {
		disabledContribResources = PredeclaredProperties.getDisabledContributions();
		for (TreeItem node : tree.getTree().getItems()) {
			setCheckBox(node, false);
		}
	}

	private void setCheckBox(TreeItem node, Boolean defaultSetting) {
		Object obj = node.getData();
		if (obj instanceof TreeNode) {
			String uriPath = ((TreeNode) obj).path;
			if (uriPath != null && !uriPath.isEmpty()) {
				URI uri = URI.createURI(uriPath);
				if (((TreeNode) obj).overridden) {
					uri = overriddenAadl.get(uri);
				}

				Boolean disabled = defaultSetting || disabledContribResources.contains(uri)
						|| (overriddenAadl.containsKey(uri)
								&& disabledContribResources.contains(overriddenAadl.get(uri)));
				node.setChecked(disabled);
			}
		}

		for (TreeItem child : node.getItems()) {
			setCheckBox(child, node.getChecked());
		}
	}

	private void doOverrideAction(final TreeNode selectedNode) {
		if (selectedNode.canOverride()) {
			URI uri = URI.createURI(selectedNode.path);
			if (uri != null) {
				final URI newURI = getWorkspaceContributedResource(uri.lastSegment());
				if (newURI != null) {
					overriddenAadl.put(uri, newURI);
					restoreButton.setEnabled(true);
					selectedNode.overridden = true;
					tree.refresh();
					uriLabel.setText(uriToReadable(newURI));
				}
			}
		}
	}

	private static URI getURIFromSelection(final ISelection selection) {
		return (URI) ((IStructuredSelection) selection).getFirstElement();
	}

	private static String uriToReadable(final URI uri) {
		final String uriAsString = uri.toString();
		final int firstSlash = uriAsString.indexOf('/');
		final int secondSlash = uriAsString.indexOf('/', firstSlash + 1);
		return uriAsString.substring(secondSlash + 1);
	}

	@Override
	public void init(final IWorkbench workbench) {
		setPreferenceStore(PluginSupportPlugin.getDefault().getPreferenceStore());
	}

	@Override
	public boolean performOk() {
		final boolean ok = super.performOk();

		/* Check if the preferences changed. Don't want to rebuild the workspace if they didn't */
		boolean changed = false;

		if (!originalOverriddenAadl.equals(overriddenAadl)) {
			PredeclaredProperties.setOverriddenResources(overriddenAadl);
			changed = true;
		}

		disabledContribResources = new ArrayList<URI>();
		for (TreeItem pluginContribNode : tree.getTree().getItems()) {
			buildDisabledContributionsList(pluginContribNode);
		}

		List<URI> oldDisabledContrib = PredeclaredProperties.getDisabledContributions();
		if (disabledContribResources.size() != oldDisabledContrib.size()
				|| !disabledContribResources.equals(oldDisabledContrib)) {
			// save disabled contribution resources to workspace preferences
			PredeclaredProperties.setDisabledContributions(disabledContribResources);
			changed = true;
		}

		if (changed) {
			PredeclaredProperties.closeAndReopenProjects();
		}

		return ok;
	}

	private void buildDisabledContributionsList(TreeItem node) {
		if (node.getChecked()) {
			Object obj = node.getData();
			if (obj instanceof TreeNode) {
				String uriPath = ((TreeNode) obj).path;
				if (uriPath != null && !uriPath.isEmpty()) {
					URI uri = URI.createURI(uriPath);
					disabledContribResources.add(uri); // both URIs should be ignored
					// check if overridden
					if (overriddenAadl.containsKey(uri)) {
						disabledContribResources.add(overriddenAadl.get(uri));
					}
				}
			}
		}

		for (TreeItem child : node.getItems()) {
			buildDisabledContributionsList(child);
		}
	}

	@Override
	public void performHelp() {
		PlatformUI.getWorkbench().getHelpSystem().setHelp(getShell(), "org.osate.ui.help_dialog_contribRes");
		PlatformUI.getWorkbench().getHelpSystem().displayHelp("org.osate.ui.help_dialog_contribRes");
	}

	private static boolean filterContainer(final Map<Object, Boolean> visible, final IResource irsrc,
			final String fileName) throws CoreException {
		boolean isViz = false;
		if (irsrc instanceof IFile) {
			isViz = irsrc.getName().equalsIgnoreCase(fileName);
		} else if (irsrc instanceof IContainer) {
			if (!(irsrc instanceof IProject) || ((IProject) irsrc).isOpen()) {
				for (final IResource child : ((IContainer) irsrc).members()) {
					isViz |= filterContainer(visible, child, fileName);
				}
			}
		}
		visible.put(irsrc, isViz);
		return isViz;
	}

	private URI getWorkspaceContributedResource(final String fileName) {
		final ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(getShell(),
				new WorkbenchLabelProvider(), new WorkbenchContentProvider());
		dialog.setTitle("Choose the Replacement Resource");
		dialog.setMessage(
				"Choose a file named \"" + fileName
						+ "\" in the workspace to override the contributed resource." + System.lineSeparator()
						+ "Only acceptable replacements are shown below.");
		dialog.setInput(ResourcesPlugin.getWorkspace().getRoot());

		final Map<Object, Boolean> visible = new HashMap<>();
		try {
			for (final IResource irsrc : ResourcesPlugin.getWorkspace().getRoot().members()) {
				filterContainer(visible, irsrc, fileName);
			}
		} catch (final CoreException e) {
			OsateUiPlugin.log(e);
		}

		dialog.setAllowMultiple(false); // only singleton selections
		dialog.addFilter(new ViewerFilter() {
			@Override
			public boolean select(final Viewer viewer, final Object parentElement, final Object element) {
				return visible.getOrDefault(element, false);
			}
		});
		dialog.setValidator(selection -> {
			/*
			 * Must a be singleton selection of an IFile whose file name is
			 * the given filename.
			 */
			if (selection.length == 1 && selection[0] instanceof IFile &&
					((IFile) selection[0]).getName().equalsIgnoreCase(fileName)) {
				return new Status(IStatus.OK, OsateUiPlugin.PLUGIN_ID, "");
			} else {
				return new Status(IStatus.ERROR, OsateUiPlugin.PLUGIN_ID,
						"Must select a file named '" + fileName + "'.");
			}
		});

		if (dialog.open() == Window.OK) {
			return URI.createPlatformResourceURI(((IResource) dialog.getFirstResult()).getFullPath().toString(), false);
		} else {
			return null;
		}
	}

	protected TreeViewer createTree(Composite parent) {
		GridData compLayout = new GridData(GridData.FILL_BOTH);
		compLayout.heightHint = 200;
		compLayout.widthHint = 200;

		Composite treeComposite = new Composite(parent, SWT.NONE);
		treeComposite.setLayoutData(compLayout);

		GridData dataLayout = new GridData(GridData.FILL_BOTH);
		dataLayout.heightHint = compLayout.heightHint;
		dataLayout.widthHint = compLayout.widthHint;

		int style = SWT.SINGLE | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL | SWT.CHECK;
		Tree tree = new Tree(treeComposite, style);
		tree.setLayoutData(dataLayout);
		tree.setLinesVisible(true);
		tree.setHeaderVisible(false);
		tree.setFont(parent.getFont());

		TreeColumn col = new TreeColumn(tree, SWT.LEFT);

		TreeColumnLayout treeLayout = new TreeColumnLayout();
		treeLayout.setColumnData(col, new ColumnWeightData(dataLayout.widthHint));
		treeComposite.setLayout(treeLayout);

		return new TreeViewer(tree);
	}

	protected TreeNode createTreeHierarchy() {
		final List<URI> contributedAadl = PluginSupportUtil.getContributedAadl();

		TreeNode root = new TreeNode();
		TreeNode cont = new TreeNode("Plug-in Contributions", 0);
		TreeNode pSet = new TreeNode("Predeclared_Property_Sets", 1);

		HashMap<URI, Boolean> isInPropSet = new HashMap<URI, Boolean>();

		PredeclaredProperties.getContributedResources().stream().forEach(uri -> {
			OptionalInt firstSignificantIndex = PluginSupportUtil.getFirstSignificantIndex(uri);
			isInPropSet.put(uri, !(!firstSignificantIndex.isPresent()
					|| firstSignificantIndex.getAsInt() == uri.segmentCount() - 1));
		});

		for (URI fullPath : contributedAadl) {
			if (isInPropSet.containsKey(fullPath) && isInPropSet.get(fullPath)) {
				pSet.addNode(new TreeNode(fullPath.toString(), fullPath.lastSegment(),
						overriddenAadl.containsKey(fullPath)));
			} else {
				cont.addNode(new TreeNode(fullPath.toString(), fullPath.lastSegment(),
						overriddenAadl.containsKey(fullPath)));
			}
		}

		cont.addNode(pSet);
		root.addNode(cont);

		return root;
	}

	public class TreeContentProvider implements ITreeContentProvider {
		Object treeContent;

		@Override
		public void dispose() {
			// Nothing to do.
		}

		@SuppressWarnings("unchecked")
		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
			treeContent = newInput;
		}

		@Override
		public Object[] getElements(Object inputElement) {
			return getChildren(inputElement);
		}

		@Override
		public Object[] getChildren(Object parentElement) {
			if (parentElement instanceof TreeNode) {
				return ((TreeNode) parentElement).getNode().toArray();
			} else {
				return null;
			}
		}

		@SuppressWarnings("unchecked")
		@Override
		public Object getParent(Object element) {
			if (element instanceof TreeNode) {
				return ((TreeNode) element).getParent();
			}

			return null;
		}

		@Override
		public boolean hasChildren(Object element) {
			return getChildren(element).length > 0;
		}
	}

	public class FileLabelProvider extends LabelProvider {
		public FileLabelProvider(Image fileImg, Image categoryImg) {
			super();
		}

		@Override
		public Image getImage(Object element) {
			Image image = null;
			if (element instanceof TreeNode) {
				switch (((TreeNode) element).imageType) {
				case 0:
					image = OsateUiPlugin.getImageDescriptor("icons/library_obj.gif").createImage();
					break;
				case 1:
					image = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER);
					break;
				default:
					image = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE);
					break;
				}
			}

			return image;
		}

		@Override
		public void dispose() {
			super.dispose();
		}

		@Override
		public String getText(Object element) {
			if (element instanceof TreeNode) {
				return ((TreeNode) element).getLabel();
			}

			return "";
		}
	}

	public class TreeNode {
		public TreeNode() {
		}

		public TreeNode(String path, String label, Boolean overridden) {
			this.path = path;
			this.label = label;
			this.overridden = overridden;
			this.imageType = 2;
		}

		public TreeNode(String label, int imageType) {
			this.label = label;
			this.overridden = false;
			this.imageType = imageType;
			this.path = "";
		}

		private String label;
		public String path;
		public Boolean overridden;
		public int imageType;

		protected List<TreeNode> nodes = new ArrayList<>();
		protected TreeNode parent;

		public Boolean canOverride() {
			return this.path != null && this.path.contains(".");
		}

		public String getLabel() {
			return (this.overridden ? "[Overridden] " : "") + this.label;
		}

		public List<TreeNode> getNode() {
			return this.nodes;
		}

		protected void addNode(TreeNode node) {
			this.nodes.add(node);
			node.parent = this;
		}

		protected TreeNode getParent() {
			return this.parent;
		}
	}

	public class Sorter extends ViewerSorter {
		@Override
		public int category(Object element) {
			if (element instanceof TreeNode) {
				if (((TreeNode) element).imageType == 1) {
					return 0;
				}
			}

			return 1 + super.category(element);
		}
	}
}