From: Mathieu Baudier Date: Tue, 1 Nov 2022 07:53:16 +0000 (+0100) Subject: Move Devops from Argeo JCR to SLC X-Git-Tag: v2.3.6~17 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=e04893a549e6c343f60f241e28f191a0a31ecd9f;p=gpl%2Fargeo-slc.git Move Devops from Argeo JCR to SLC --- diff --git a/Makefile b/Makefile index 08c9add49..3e3fd9e5b 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,8 @@ org.argeo.slc.cms \ org.argeo.slc.repo \ org.argeo.slc.rpmfactory \ org.argeo.slc.jcr \ +swt/org.argeo.tool.swt \ +swt/org.argeo.tool.devops.e4 \ swt/rap/org.argeo.tool.rap.cli \ swt/rap/org.argeo.tool.server \ @@ -35,6 +37,7 @@ org.argeo.tp.gis \ org.argeo.cms \ org.argeo.cms.jcr \ swt/org.argeo.cms \ +swt/org.argeo.cms.jcr \ swt/rap/org.argeo.cms \ clean: diff --git a/swt/org.argeo.tool.devops.e4/.classpath b/swt/org.argeo.tool.devops.e4/.classpath new file mode 100644 index 000000000..81fe078c2 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/swt/org.argeo.tool.devops.e4/.project b/swt/org.argeo.tool.devops.e4/.project new file mode 100644 index 000000000..6a3ee57d5 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/.project @@ -0,0 +1,33 @@ + + + org.argeo.tool.devops.e4 + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/swt/org.argeo.tool.devops.e4/OSGI-INF/cmsAdminRap.xml b/swt/org.argeo.tool.devops.e4/OSGI-INF/cmsAdminRap.xml new file mode 100644 index 000000000..70f4ae90a --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/OSGI-INF/cmsAdminRap.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/swt/org.argeo.tool.devops.e4/OSGI-INF/homeRepository.xml b/swt/org.argeo.tool.devops.e4/OSGI-INF/homeRepository.xml new file mode 100644 index 000000000..2722aabb6 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/OSGI-INF/homeRepository.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/swt/org.argeo.tool.devops.e4/OSGI-INF/userAdminWrapper.xml b/swt/org.argeo.tool.devops.e4/OSGI-INF/userAdminWrapper.xml new file mode 100644 index 000000000..cc7087b6e --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/OSGI-INF/userAdminWrapper.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/swt/org.argeo.tool.devops.e4/bnd.bnd b/swt/org.argeo.tool.devops.e4/bnd.bnd new file mode 100644 index 000000000..7abfffe64 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/bnd.bnd @@ -0,0 +1,19 @@ +Bundle-ActivationPolicy: lazy + +Import-Package: \ +org.eclipse.swt,\ +org.eclipse.swt.widgets;version="0.0.0",\ +org.eclipse.jface.window,\ +org.eclipse.core.commands.common,\ +org.eclipse.e4.ui.model.application.ui,\ +org.eclipse.e4.ui.model.application,\ +javax.jcr.nodetype,\ +org.apache.jackrabbit.*;version="[2,3)",\ +org.argeo.cms,\ +org.argeo.jcr,\ +org.argeo.api.acr,\ +* + +Service-Component: OSGI-INF/homeRepository.xml,\ +OSGI-INF/userAdminWrapper.xml,\ +OSGI-INF/cmsAdminRap.xml,\ diff --git a/swt/org.argeo.tool.devops.e4/build.properties b/swt/org.argeo.tool.devops.e4/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/swt/org.argeo.tool.devops.e4/e4xmi/devops.e4xmi b/swt/org.argeo.tool.devops.e4/e4xmi/devops.e4xmi new file mode 100644 index 000000000..d9361b83c --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/e4xmi/devops.e4xmi @@ -0,0 +1,129 @@ + + + + + shellMaximized + auth.cn=admin,ou=roles,ou=node + + + auth.cn=admin,ou=roles,ou=node + + + + + + + + + + + + + usersEditorArea + + + + + + + + + + + + + + + + + + + + + + + + + ViewMenu + + + + + + + + + + dataExplorer + + + + + + + + + + + + + + + + + + + + + + + + + auth.cn=admin,ou=roles,ou=node + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java new file mode 100644 index 000000000..aabfbf5dd --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java @@ -0,0 +1,47 @@ +package org.argeo.cms.e4.files; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.spi.FileSystemProvider; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.argeo.eclipse.ui.fs.SimpleFsBrowser; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; + +/** Browse the node file system. */ +public class NodeFsBrowserView { + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + + // ".nodeFsBrowserView"; + + @Inject + FileSystemProvider nodeFileSystemProvider; + + @PostConstruct + public void createPartControl(Composite parent) { + try { + // URI uri = new URI("node://root:demo@localhost:7070/"); + URI uri = new URI("node:///"); + FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri); + if (fileSystem == null) + fileSystem = nodeFileSystemProvider.newFileSystem(uri, null); + Path nodePath = fileSystem.getPath("/"); + + Path localPath = Paths.get(System.getProperty("user.home")); + + SimpleFsBrowser browser = new SimpleFsBrowser(parent, SWT.NO_FOCUS); + browser.setInput(nodePath, localPath); +// AdvancedFsBrowser browser = new AdvancedFsBrowser(); +// browser.createUi(parent, localPath); + } catch (Exception e) { + throw new RuntimeException("Cannot open file system browser", e); + } + } + + public void setFocus() { + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/package-info.java new file mode 100644 index 000000000..b481dd48a --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/files/package-info.java @@ -0,0 +1,2 @@ +/** Files browser perspective. */ +package org.argeo.cms.e4.files; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/EclipseJcrMonitor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/EclipseJcrMonitor.java new file mode 100644 index 000000000..e10738ee0 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/EclipseJcrMonitor.java @@ -0,0 +1,44 @@ +package org.argeo.cms.e4.jcr; + +import org.argeo.jcr.JcrMonitor; +import org.eclipse.core.runtime.IProgressMonitor; + +/** + * Wraps an Eclipse {@link IProgressMonitor} so that it can be passed to + * framework agnostic Argeo routines. + */ +public class EclipseJcrMonitor implements JcrMonitor { + private final IProgressMonitor progressMonitor; + + public EclipseJcrMonitor(IProgressMonitor progressMonitor) { + this.progressMonitor = progressMonitor; + } + + public void beginTask(String name, int totalWork) { + progressMonitor.beginTask(name, totalWork); + } + + public void done() { + progressMonitor.done(); + } + + public boolean isCanceled() { + return progressMonitor.isCanceled(); + } + + public void setCanceled(boolean value) { + progressMonitor.setCanceled(value); + } + + public void setTaskName(String name) { + progressMonitor.setTaskName(name); + } + + public void subTask(String name) { + progressMonitor.subTask(name); + } + + public void worked(int work) { + progressMonitor.worked(work); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java new file mode 100644 index 000000000..e17f17bb7 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java @@ -0,0 +1,141 @@ +package org.argeo.cms.e4.jcr; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import org.argeo.cms.ui.jcr.PropertyLabelProvider; +import org.argeo.eclipse.ui.EclipseUiException; +import org.eclipse.jface.layout.TreeColumnLayout; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.IBaseLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +/** + * Generic editor property page. Lists all properties of current node as a + * complex tree. TODO: enable editing + */ +public class GenericPropertyPage { + + // Main business Objects + private Node currentNode; + + public GenericPropertyPage(Node currentNode) { + this.currentNode = currentNode; + } + + protected void createFormContent(Composite parent) { + Composite innerBox = new Composite(parent, SWT.NONE); + // Composite innerBox = new Composite(body, SWT.NO_FOCUS); + FillLayout layout = new FillLayout(); + layout.marginHeight = 5; + layout.marginWidth = 5; + innerBox.setLayout(layout); + createComplexTree(innerBox); + // TODO TreeColumnLayout triggers a scroll issue with the form: + // The inside body is always to big and a scroll bar is shown + // Composite tableCmp = new Composite(body, SWT.NO_FOCUS); + // createComplexTree(tableCmp); + } + + private TreeViewer createComplexTree(Composite parent) { + int style = SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION; + Tree tree = new Tree(parent, style); + TreeColumnLayout tableColumnLayout = new TreeColumnLayout(); + + createColumn(tree, tableColumnLayout, "Property", SWT.LEFT, 200, 30); + createColumn(tree, tableColumnLayout, "Value(s)", SWT.LEFT, 300, 60); + createColumn(tree, tableColumnLayout, "Type", SWT.LEFT, 75, 10); + createColumn(tree, tableColumnLayout, "Attributes", SWT.LEFT, 75, 0); + // Do not apply the treeColumnLayout it does not work yet + // parent.setLayout(tableColumnLayout); + + tree.setLinesVisible(true); + tree.setHeaderVisible(true); + + TreeViewer treeViewer = new TreeViewer(tree); + treeViewer.setContentProvider(new TreeContentProvider()); + treeViewer.setLabelProvider((IBaseLabelProvider) new PropertyLabelProvider()); + treeViewer.setInput(currentNode); + treeViewer.expandAll(); + return treeViewer; + } + + private static TreeColumn createColumn(Tree parent, TreeColumnLayout tableColumnLayout, String name, int style, + int width, int weight) { + TreeColumn column = new TreeColumn(parent, style); + column.setText(name); + column.setWidth(width); + column.setMoveable(true); + column.setResizable(true); + tableColumnLayout.setColumnData(column, new ColumnWeightData(weight, width, true)); + return column; + } + + private class TreeContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = -6162736530019406214L; + + public Object[] getElements(Object parent) { + Object[] props = null; + try { + + if (parent instanceof Node) { + Node node = (Node) parent; + PropertyIterator pi; + pi = node.getProperties(); + List propList = new ArrayList(); + while (pi.hasNext()) { + propList.add(pi.nextProperty()); + } + props = propList.toArray(); + } + } catch (RepositoryException e) { + throw new EclipseUiException("Unexpected exception while listing node properties", e); + } + return props; + } + + public Object getParent(Object child) { + return null; + } + + public Object[] getChildren(Object parent) { + if (parent instanceof Property) { + Property prop = (Property) parent; + try { + if (prop.isMultiple()) + return prop.getValues(); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot get multi-prop values on " + prop, e); + } + } + return null; + } + + public boolean hasChildren(Object parent) { + try { + return (parent instanceof Property && ((Property) parent).isMultiple()); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot check if property is multiple for " + parent, e); + } + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + public void dispose() { + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java new file mode 100644 index 000000000..0b77c0732 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java @@ -0,0 +1,349 @@ +package org.argeo.cms.e4.jcr; + +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.security.CryptoKeyring; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.jcr.JcrBrowserUtils; +import org.argeo.cms.ui.jcr.NodeContentProvider; +import org.argeo.cms.ui.jcr.NodeLabelProvider; +import org.argeo.cms.ui.jcr.OsgiRepositoryRegister; +import org.argeo.cms.ui.jcr.PropertiesContentProvider; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.jcr.AsyncUiEventListener; +import org.argeo.eclipse.ui.jcr.util.NodeViewerComparer; +import org.argeo.jcr.JcrUtils; +import org.eclipse.e4.core.contexts.IEclipseContext; +import org.eclipse.e4.core.di.annotations.Optional; +import org.eclipse.e4.ui.services.EMenuService; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IBaseLabelProvider; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +/** + * Basic View to display a sash form to browse a JCR compliant multiple + * repository environment + */ +public class JcrBrowserView { + final static String ID = "org.argeo.cms.e4.jcrbrowser"; + final static String NODE_VIEWER_POPUP_MENU_ID = "org.argeo.cms.e4.popupmenu.nodeViewer"; + + private boolean sortChildNodes = true; + + /* DEPENDENCY INJECTION */ + @Inject + @Optional + private Keyring keyring; + @Inject + private RepositoryFactory repositoryFactory; + @Inject + private Repository nodeRepository; + + // Current user session on the home repository default workspace + private Session userSession; + + private OsgiRepositoryRegister repositoryRegister = new OsgiRepositoryRegister(); + + // This page widgets + private TreeViewer nodesViewer; + private NodeContentProvider nodeContentProvider; + private TableViewer propertiesViewer; + private EventListener resultsObserver; + + @PostConstruct + public void createPartControl(Composite parent, IEclipseContext context, EPartService partService, + ESelectionService selectionService, EMenuService menuService) { + repositoryRegister.init(); + + parent.setLayout(new FillLayout()); + SashForm sashForm = new SashForm(parent, SWT.VERTICAL); + // sashForm.setSashWidth(4); + // sashForm.setLayout(new FillLayout()); + + // Create the tree on top of the view + Composite top = new Composite(sashForm, SWT.NONE); + // GridLayout gl = new GridLayout(1, false); + top.setLayout(CmsSwtUtils.noSpaceGridLayout()); + + try { + this.userSession = this.nodeRepository.login(CmsConstants.HOME_WORKSPACE); + } catch (RepositoryException e) { + throw new CmsException("Cannot open user session", e); + } + + nodeContentProvider = new NodeContentProvider(userSession, keyring, repositoryRegister, repositoryFactory, + sortChildNodes); + + // nodes viewer + nodesViewer = createNodeViewer(top, nodeContentProvider); + + // context menu : it is completely defined in the plugin.xml file. + // MenuManager menuManager = new MenuManager(); + // Menu menu = menuManager.createContextMenu(nodesViewer.getTree()); + + // nodesViewer.getTree().setMenu(menu); + + nodesViewer.setInput(""); + + // Create the property viewer on the bottom + Composite bottom = new Composite(sashForm, SWT.NONE); + bottom.setLayout(CmsSwtUtils.noSpaceGridLayout()); + propertiesViewer = createPropertiesViewer(bottom); + + sashForm.setWeights(getWeights()); + nodesViewer.setComparer(new NodeViewerComparer()); + nodesViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + selectionService.setSelection(selection.toList()); + } + }); + nodesViewer.addDoubleClickListener(new JcrE4DClickListener(nodesViewer, partService)); + menuService.registerContextMenu(nodesViewer.getControl(), NODE_VIEWER_POPUP_MENU_ID); + // getSite().registerContextMenu(menuManager, nodesViewer); + // getSite().setSelectionProvider(nodesViewer); + } + + @PreDestroy + public void dispose() { + JcrUtils.logoutQuietly(userSession); + repositoryRegister.destroy(); + } + + public void refresh(Object obj) { + // Enable full refresh from a command when no element of the tree is + // selected + if (obj == null) { + Object[] elements = nodeContentProvider.getElements(null); + for (Object el : elements) { + if (el instanceof TreeParent) + JcrBrowserUtils.forceRefreshIfNeeded((TreeParent) el); + getNodeViewer().refresh(el); + } + } else + getNodeViewer().refresh(obj); + } + + /** + * To be overridden to adapt size of form and result frames. + */ + protected int[] getWeights() { + return new int[] { 70, 30 }; + } + + protected TreeViewer createNodeViewer(Composite parent, final ITreeContentProvider nodeContentProvider) { + + final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.MULTI); + + tmpNodeViewer.getTree().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + tmpNodeViewer.setContentProvider(nodeContentProvider); + tmpNodeViewer.setLabelProvider((IBaseLabelProvider) new NodeLabelProvider()); + tmpNodeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + if (!event.getSelection().isEmpty()) { + IStructuredSelection sel = (IStructuredSelection) event.getSelection(); + Object firstItem = sel.getFirstElement(); + if (firstItem instanceof SingleJcrNodeElem) + propertiesViewer.setInput(((SingleJcrNodeElem) firstItem).getNode()); + } else { + propertiesViewer.setInput(""); + } + } + }); + + resultsObserver = new TreeObserver(tmpNodeViewer.getTree().getDisplay()); + if (keyring != null) + try { + ObservationManager observationManager = userSession.getWorkspace().getObservationManager(); + observationManager.addEventListener(resultsObserver, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/", + true, null, null, false); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot register listeners", e); + } + + // tmpNodeViewer.addDoubleClickListener(new JcrDClickListener(tmpNodeViewer)); + return tmpNodeViewer; + } + + protected TableViewer createPropertiesViewer(Composite parent) { + propertiesViewer = new TableViewer(parent, SWT.NONE); + propertiesViewer.getTable().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + propertiesViewer.getTable().setHeaderVisible(true); + propertiesViewer.setContentProvider(new PropertiesContentProvider()); + TableViewerColumn col = new TableViewerColumn(propertiesViewer, SWT.NONE); + col.getColumn().setText("Name"); + col.getColumn().setWidth(200); + col.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -6684361063107478595L; + + public String getText(Object element) { + try { + return ((Property) element).getName(); + } catch (RepositoryException e) { + throw new EclipseUiException("Unexpected exception in label provider", e); + } + } + }); + col = new TableViewerColumn(propertiesViewer, SWT.NONE); + col.getColumn().setText("Value"); + col.getColumn().setWidth(400); + col.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -8201994187693336657L; + + public String getText(Object element) { + try { + Property property = (Property) element; + if (property.getType() == PropertyType.BINARY) + return ""; + else if (property.isMultiple()) { + StringBuffer buf = new StringBuffer("["); + Value[] values = property.getValues(); + for (int i = 0; i < values.length; i++) { + if (i != 0) + buf.append(", "); + buf.append(values[i].getString()); + } + buf.append(']'); + return buf.toString(); + } else + return property.getValue().getString(); + } catch (RepositoryException e) { + throw new EclipseUiException("Unexpected exception in label provider", e); + } + } + }); + col = new TableViewerColumn(propertiesViewer, SWT.NONE); + col.getColumn().setText("Type"); + col.getColumn().setWidth(200); + col.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -6009599998150286070L; + + public String getText(Object element) { + return JcrBrowserUtils.getPropertyTypeAsString((Property) element); + } + }); + propertiesViewer.setInput(""); + return propertiesViewer; + } + + protected TreeViewer getNodeViewer() { + return nodesViewer; + } + + /** + * Resets the tree content provider + * + * @param sortChildNodes if true the content provider will use a comparer to + * sort nodes that might slow down the display + */ + public void setSortChildNodes(boolean sortChildNodes) { + this.sortChildNodes = sortChildNodes; + ((NodeContentProvider) nodesViewer.getContentProvider()).setSortChildren(sortChildNodes); + nodesViewer.setInput(""); + } + + /** Notifies the current view that a node has been added */ + public void nodeAdded(TreeParent parentNode) { + // insure that Ui objects have been correctly created: + JcrBrowserUtils.forceRefreshIfNeeded(parentNode); + getNodeViewer().refresh(parentNode); + getNodeViewer().expandToLevel(parentNode, 1); + } + + /** Notifies the current view that a node has been removed */ + public void nodeRemoved(TreeParent parentNode) { + IStructuredSelection newSel = new StructuredSelection(parentNode); + getNodeViewer().setSelection(newSel, true); + // Force refresh + IStructuredSelection tmpSel = (IStructuredSelection) getNodeViewer().getSelection(); + getNodeViewer().refresh(tmpSel.getFirstElement()); + } + + class TreeObserver extends AsyncUiEventListener { + + public TreeObserver(Display display) { + super(display); + } + + @Override + protected Boolean willProcessInUiThread(List events) throws RepositoryException { + for (Event event : events) { + if (getLog().isTraceEnabled()) + getLog().debug("Received event " + event); + String path = event.getPath(); + int index = path.lastIndexOf('/'); + String propertyName = path.substring(index + 1); + if (getLog().isTraceEnabled()) + getLog().debug("Concerned property " + propertyName); + } + return false; + } + + protected void onEventInUiThread(List events) throws RepositoryException { + if (getLog().isTraceEnabled()) + getLog().trace("Refresh result list"); + nodesViewer.refresh(); + } + + } + + public boolean getSortChildNodes() { + return sortChildNodes; + } + + public void setFocus() { + getNodeViewer().getTree().setFocus(); + } + + /* DEPENDENCY INJECTION */ + // public void setRepositoryRegister(RepositoryRegister repositoryRegister) { + // this.repositoryRegister = repositoryRegister; + // } + + public void setKeyring(CryptoKeyring keyring) { + this.keyring = keyring; + } + + public void setRepositoryFactory(RepositoryFactory repositoryFactory) { + this.repositoryFactory = repositoryFactory; + } + + public void setNodeRepository(Repository nodeRepository) { + this.nodeRepository = nodeRepository; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java new file mode 100644 index 000000000..f4ee2e8da --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java @@ -0,0 +1,36 @@ +package org.argeo.cms.e4.jcr; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.ui.jcr.JcrDClickListener; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.EPartService.PartState; +import org.eclipse.jface.viewers.TreeViewer; + +public class JcrE4DClickListener extends JcrDClickListener { + EPartService partService; + + public JcrE4DClickListener(TreeViewer nodeViewer, EPartService partService) { + super(nodeViewer); + this.partService = partService; + } + + @Override + protected void openNode(Node node) { + MPart part = partService.createPart(JcrNodeEditor.DESCRIPTOR_ID); + try { + part.setLabel(node.getName()); + part.getPersistedState().put("nodeWorkspace", node.getSession().getWorkspace().getName()); + part.getPersistedState().put("nodePath", node.getPath()); + } catch (RepositoryException e) { + throw new CmsException("Cannot open " + node, e); + } + + // the provided part is be shown + partService.showPart(part, PartState.ACTIVATE); + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java new file mode 100644 index 000000000..ae2b325f5 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java @@ -0,0 +1,26 @@ +package org.argeo.cms.e4.jcr; + +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.jcr.Node; + +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; + +public class JcrNodeEditor { + final static String DESCRIPTOR_ID = "org.argeo.cms.e4.partdescriptor.nodeEditor"; + + @PostConstruct + public void createUi(Composite parent, MPart part, ESelectionService selectionService) { + parent.setLayout(new FillLayout()); + List selection = (List) selectionService.getSelection(); + Node node = ((SingleJcrNodeElem) selection.get(0)).getNode(); + GenericPropertyPage propertyPage = new GenericPropertyPage(node); + propertyPage.createFormContent(parent); + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/SimplePart.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/SimplePart.java new file mode 100644 index 000000000..17d8d2a23 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/SimplePart.java @@ -0,0 +1,19 @@ +package org.argeo.cms.e4.jcr; + +import javax.annotation.PostConstruct; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; + +public class SimplePart { + + @PostConstruct + void init(Composite parent) { + parent.setLayout(new GridLayout()); + Label label = new Label(parent, SWT.NONE); + label.setText("Hello e4 World"); + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java new file mode 100644 index 000000000..09fa760cd --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java @@ -0,0 +1,67 @@ +package org.argeo.cms.e4.jcr.handlers; + +import java.util.List; + +import javax.inject.Named; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.argeo.cms.e4.jcr.JcrBrowserView; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ui.jcr.model.WorkspaceElem; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; +import org.argeo.eclipse.ui.dialogs.SingleValue; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; + +/** + * Adds a node of type nt:folder, only on {@link SingleJcrNodeElem} and + * {@link WorkspaceElem} TreeObject types. + * + * This handler assumes that a selection provider is available and picks only + * first selected item. It is UI's job to enable the command only when the + * selection contains one and only one element. Thus no parameter is passed + * through the command. + */ +public class AddFolderNode { + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) { + List selection = (List) selectionService.getSelection(); + JcrBrowserView view = (JcrBrowserView) part.getObject(); + + if (selection != null && selection.size() == 1) { + TreeParent treeParentNode = null; + Node jcrParentNode = null; + Object obj = selection.get(0); + + if (obj instanceof SingleJcrNodeElem) { + treeParentNode = (TreeParent) obj; + jcrParentNode = ((SingleJcrNodeElem) treeParentNode).getNode(); + } else if (obj instanceof WorkspaceElem) { + treeParentNode = (TreeParent) obj; + jcrParentNode = ((WorkspaceElem) treeParentNode).getRootNode(); + } else + return; + + String folderName = SingleValue.ask("Folder name", "Enter folder name"); + if (folderName != null) { + try { + jcrParentNode.addNode(folderName, NodeType.NT_FOLDER); + jcrParentNode.getSession().save(); + view.nodeAdded(treeParentNode); + } catch (RepositoryException e) { + ErrorFeedback.show("Cannot create folder " + folderName + " under " + treeParentNode, e); + } + } + } else { + // ErrorFeedback.show(WorkbenchUiPlugin + // .getMessage("errorUnvalidNtFolderNodeType")); + ErrorFeedback.show("Invalid NT folder node type"); + } + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java new file mode 100644 index 000000000..dc47f6edf --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java @@ -0,0 +1,210 @@ +package org.argeo.cms.e4.jcr.handlers; + +import java.net.URI; +import java.util.Hashtable; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.ArgeoNames; +import org.argeo.cms.ArgeoTypes; +import org.argeo.cms.e4.jcr.JcrBrowserView; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; +import org.argeo.jcr.JcrUtils; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.core.di.annotations.Optional; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Connect to a remote repository and, if successful publish it as an OSGi + * service. + */ +public class AddRemoteRepository { + + @Inject + private RepositoryFactory repositoryFactory; + @Inject + private Repository nodeRepository; + @Inject + @Optional + private Keyring keyring; + + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part) { + JcrBrowserView view = (JcrBrowserView) part.getObject(); + RemoteRepositoryLoginDialog dlg = new RemoteRepositoryLoginDialog(Display.getDefault().getActiveShell()); + if (dlg.open() == Dialog.OK) { + view.refresh(null); + } + } + + // public void setRepositoryFactory(RepositoryFactory repositoryFactory) { + // this.repositoryFactory = repositoryFactory; + // } + // + // public void setKeyring(Keyring keyring) { + // this.keyring = keyring; + // } + // + // public void setNodeRepository(Repository nodeRepository) { + // this.nodeRepository = nodeRepository; + // } + + class RemoteRepositoryLoginDialog extends TitleAreaDialog { + private static final long serialVersionUID = 2234006887750103399L; + private Text name; + private Text uri; + private Text username; + private Text password; + private Button saveInKeyring; + + public RemoteRepositoryLoginDialog(Shell parentShell) { + super(parentShell); + } + + protected Point getInitialSize() { + return new Point(600, 400); + } + + protected Control createDialogArea(Composite parent) { + Composite dialogarea = (Composite) super.createDialogArea(parent); + dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + Composite composite = new Composite(dialogarea, SWT.NONE); + composite.setLayout(new GridLayout(2, false)); + composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + setMessage("Login to remote repository", IMessageProvider.NONE); + name = createLT(composite, "Name", "remoteRepository"); + uri = createLT(composite, "URI", "http://localhost:7070/jcr/node"); + username = createLT(composite, "User", ""); + password = createLP(composite, "Password"); + + saveInKeyring = createLC(composite, "Remember password", false); + parent.pack(); + return composite; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + super.createButtonsForButtonBar(parent); + Button test = createButton(parent, 2, "Test", false); + test.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = -1829962269440419560L; + + public void widgetSelected(SelectionEvent arg0) { + testConnection(); + } + }); + } + + void testConnection() { + Session session = null; + try { + URI checkedUri = new URI(uri.getText()); + String checkedUriStr = checkedUri.toString(); + + Hashtable params = new Hashtable(); + params.put(CmsConstants.LABELED_URI, checkedUriStr); + Repository repository = repositoryFactory.getRepository(params); + if (username.getText().trim().equals("")) {// anonymous + // FIXME make it more generic + session = repository.login(CmsConstants.SYS_WORKSPACE); + } else { + // FIXME use getTextChars() when upgrading to 3.7 + // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=297412 + char[] pwd = password.getText().toCharArray(); + SimpleCredentials sc = new SimpleCredentials(username.getText(), pwd); + session = repository.login(sc, "main"); + MessageDialog.openInformation(getParentShell(), "Success", + "Connection to '" + uri.getText() + "' successful"); + } + } catch (Exception e) { + ErrorFeedback.show("Connection test failed for " + uri.getText(), e); + } finally { + JcrUtils.logoutQuietly(session); + } + } + + @Override + protected void okPressed() { + Session nodeSession = null; + try { + nodeSession = nodeRepository.login(); + Node home = CmsJcrUtils.getUserHome(nodeSession); + + Node remote = home.hasNode(ArgeoNames.ARGEO_REMOTE) ? home.getNode(ArgeoNames.ARGEO_REMOTE) + : home.addNode(ArgeoNames.ARGEO_REMOTE); + if (remote.hasNode(name.getText())) + throw new EclipseUiException("There is already a remote repository named " + name.getText()); + Node remoteRepository = remote.addNode(name.getText(), ArgeoTypes.ARGEO_REMOTE_REPOSITORY); + remoteRepository.setProperty(ArgeoNames.ARGEO_URI, uri.getText()); + remoteRepository.setProperty(ArgeoNames.ARGEO_USER_ID, username.getText()); + nodeSession.save(); + if (saveInKeyring.getSelection()) { + String pwdPath = remoteRepository.getPath() + '/' + ArgeoNames.ARGEO_PASSWORD; + keyring.set(pwdPath, password.getText().toCharArray()); + } + nodeSession.save(); + MessageDialog.openInformation(getParentShell(), "Repository Added", + "Remote repository '" + username.getText() + "@" + uri.getText() + "' added"); + + super.okPressed(); + } catch (Exception e) { + ErrorFeedback.show("Cannot add remote repository", e); + } finally { + JcrUtils.logoutQuietly(nodeSession); + } + } + + /** Creates label and text. */ + protected Text createLT(Composite parent, String label, String initial) { + new Label(parent, SWT.NONE).setText(label); + Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER); + text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + text.setText(initial); + return text; + } + + /** Creates label and check. */ + protected Button createLC(Composite parent, String label, Boolean initial) { + new Label(parent, SWT.NONE).setText(label); + Button check = new Button(parent, SWT.CHECK); + check.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + check.setSelection(initial); + return check; + } + + protected Text createLP(Composite parent, String label) { + new Label(parent, SWT.NONE).setText(label); + Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER | SWT.PASSWORD); + text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + return text; + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java new file mode 100644 index 000000000..b8de06b46 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java @@ -0,0 +1,95 @@ +package org.argeo.cms.e4.jcr.handlers; + +import java.util.List; + +import javax.inject.Named; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.e4.jcr.JcrBrowserView; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ui.jcr.model.WorkspaceElem; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; + +/** + * Delete the selected nodes: both in the JCR repository and in the UI view. + * Warning no check is done, except implementation dependent native checks, + * handle with care. + * + * This handler is still 'hard linked' to a GenericJcrBrowser view to enable + * correct tree refresh when a node is added. This must be corrected in future + * versions. + */ +public class DeleteNodes { + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) { + List selection = (List) selectionService.getSelection(); + if (selection == null) + return; + + JcrBrowserView view = (JcrBrowserView) part.getObject(); + + // confirmation + StringBuffer buf = new StringBuffer(""); + for (Object o : selection) { + SingleJcrNodeElem sjn = (SingleJcrNodeElem) o; + buf.append(sjn.getName()).append(' '); + } + Boolean doRemove = MessageDialog.openConfirm(Display.getCurrent().getActiveShell(), "Confirm deletion", + "Do you want to delete " + buf + "?"); + + // operation + if (doRemove) { + SingleJcrNodeElem ancestor = null; + WorkspaceElem rootAncestor = null; + try { + for (Object obj : selection) { + if (obj instanceof SingleJcrNodeElem) { + // Cache objects + SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj; + TreeParent tp = (TreeParent) sjn.getParent(); + Node node = sjn.getNode(); + + // Jcr Remove + node.remove(); + node.getSession().save(); + // UI remove + tp.removeChild(sjn); + + // Check if the parent is the root node + if (tp instanceof WorkspaceElem) + rootAncestor = (WorkspaceElem) tp; + else + ancestor = getOlder(ancestor, (SingleJcrNodeElem) tp); + } + } + if (rootAncestor != null) + view.nodeRemoved(rootAncestor); + else if (ancestor != null) + view.nodeRemoved(ancestor); + } catch (Exception e) { + ErrorFeedback.show("Cannot delete selected node ", e); + } + } + } + + private SingleJcrNodeElem getOlder(SingleJcrNodeElem A, SingleJcrNodeElem B) { + try { + if (A == null) + return B == null ? null : B; + // Todo enhanced this method + else + return A.getNode().getDepth() <= B.getNode().getDepth() ? A : B; + } catch (RepositoryException re) { + throw new EclipseUiException("Cannot find ancestor", re); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java new file mode 100644 index 000000000..036e70ac1 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java @@ -0,0 +1,43 @@ +package org.argeo.cms.e4.jcr.handlers; + +import java.util.List; + +import javax.inject.Named; + +import org.argeo.cms.e4.jcr.JcrBrowserView; +import org.argeo.cms.ui.jcr.JcrBrowserUtils; +import org.argeo.cms.ux.widgets.TreeParent; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; + +/** + * Force the selected objects of the active view to be refreshed doing the + * following: + *
    + *
  1. The model objects are recomputed
  2. + *
  3. the view is refreshed
  4. + *
+ */ +public class Refresh { + + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, EPartService partService, + ESelectionService selectionService) { + + JcrBrowserView view = (JcrBrowserView) part.getObject(); + List selection = (List) selectionService.getSelection(); + + if (selection != null && !selection.isEmpty()) { + for (Object obj : selection) + if (obj instanceof TreeParent) { + TreeParent tp = (TreeParent) obj; + JcrBrowserUtils.forceRefreshIfNeeded(tp); + view.refresh(obj); + } + } else if (view instanceof JcrBrowserView) + view.refresh(null); // force full refresh + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java new file mode 100644 index 000000000..97674abc2 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java @@ -0,0 +1,59 @@ +package org.argeo.cms.e4.jcr.handlers; + +import java.util.List; + +import javax.inject.Named; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.cms.e4.jcr.JcrBrowserView; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.dialogs.SingleValue; +import org.argeo.jcr.JcrUtils; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; + +/** + * Canonically call JCR Session#move(String, String) on the first element + * returned by HandlerUtil#getActiveWorkbenchWindow() + * (...getActivePage().getSelection()), if it is a {@link SingleJcrNodeElem}. + * The user must then fill a new name in and confirm + */ +public class RenameNode { + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, EPartService partService, + ESelectionService selectionService) { + List selection = (List) selectionService.getSelection(); + if (selection == null || selection.size() != 1) + return; + JcrBrowserView view = (JcrBrowserView) part.getObject(); + + Object element = selection.get(0); + if (element instanceof SingleJcrNodeElem) { + SingleJcrNodeElem sjn = (SingleJcrNodeElem) element; + Node node = sjn.getNode(); + Session session = null; + String newName = null; + String oldPath = null; + try { + newName = SingleValue.ask("New node name", "Please provide a new name for [" + node.getName() + "]"); + // TODO sanity check and user feedback + newName = JcrUtils.replaceInvalidChars(newName); + oldPath = node.getPath(); + session = node.getSession(); + session.move(oldPath, JcrUtils.parentPath(oldPath) + "/" + newName); + session.save(); + + // Manually refresh the browser view. Must be enhanced + view.refresh(sjn); + } catch (RepositoryException e) { + throw new EclipseUiException("Unable to rename " + node + " to " + newName, e); + } + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/package-info.java new file mode 100644 index 000000000..4e075e2b1 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/handlers/package-info.java @@ -0,0 +1,2 @@ +/** JCR browser handlers. */ +package org.argeo.cms.e4.jcr.handlers; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/package-info.java new file mode 100644 index 000000000..3e92fb04d --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/jcr/package-info.java @@ -0,0 +1,2 @@ +/** JCR browser perspective. */ +package org.argeo.cms.e4.jcr; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/AbstractOsgiComposite.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/AbstractOsgiComposite.java new file mode 100644 index 000000000..4fd1d68dc --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/AbstractOsgiComposite.java @@ -0,0 +1,41 @@ +package org.argeo.cms.e4.maintenance; + +import java.util.Collection; + +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +abstract class AbstractOsgiComposite extends Composite { + private static final long serialVersionUID = -4097415973477517137L; + protected final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext(); + protected final CmsLog log = CmsLog.getLog(getClass()); + + public AbstractOsgiComposite(Composite parent, int style) { + super(parent, style); + parent.setLayout(CmsSwtUtils.noSpaceGridLayout()); + setLayout(CmsSwtUtils.noSpaceGridLayout()); + setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + initUi(style); + } + + protected abstract void initUi(int style); + + protected T getService(Class clazz) { + return bc.getService(bc.getServiceReference(clazz)); + } + + protected Collection> getServiceReferences(Class clazz, String filter) { + try { + return bc.getServiceReferences(clazz, filter); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Filter " + filter + " is invalid", e); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/Browse.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/Browse.java new file mode 100644 index 000000000..a536da066 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/Browse.java @@ -0,0 +1,576 @@ +package org.argeo.cms.e4.maintenance; + +import static org.eclipse.swt.SWT.RIGHT; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.LinkedHashMap; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.CmsUiProvider; +import org.argeo.cms.ui.util.CmsLink; +import org.argeo.cms.ui.widgets.EditableImage; +import org.argeo.cms.ui.widgets.Img; +import org.argeo.jcr.JcrUtils; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ILazyContentProvider; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; + +public class Browse implements CmsUiProvider { + + // Some local constants to experiment. should be cleaned + private final static String BROWSE_PREFIX = "browse#"; + private final static int THUMBNAIL_WIDTH = 400; + private final static int COLUMN_WIDTH = 160; + private DateFormat timeFormatter = new SimpleDateFormat("dd-MM-yyyy', 'HH:mm"); + + // keep a cache of the opened nodes + // Key is the path + private LinkedHashMap browserCols = new LinkedHashMap(); + private Composite nodeDisplayParent; + private Composite colViewer; + private ScrolledComposite scrolledCmp; + private Text parentPathTxt; + private Text filterTxt; + private Node currEdited; + + private String initialPath; + + @Override + public Control createUi(Composite parent, Node context) throws RepositoryException { + if (context == null) + // return null; + throw new CmsException("Context cannot be null"); + GridLayout layout = CmsSwtUtils.noSpaceGridLayout(); + layout.numColumns = 2; + parent.setLayout(layout); + + // Left + Composite leftCmp = new Composite(parent, SWT.NO_FOCUS); + leftCmp.setLayoutData(CmsSwtUtils.fillAll()); + createBrowserPart(leftCmp, context); + + // Right + nodeDisplayParent = new Composite(parent, SWT.NO_FOCUS | SWT.BORDER); + GridData gd = new GridData(SWT.RIGHT, SWT.FILL, false, true); + gd.widthHint = THUMBNAIL_WIDTH; + nodeDisplayParent.setLayoutData(gd); + createNodeView(nodeDisplayParent, context); + + // INIT + setEdited(context); + initialPath = context.getPath(); + + // Workaround we don't yet manage the delete to display parent of the + // initial context node + + return null; + } + + private void createBrowserPart(Composite parent, Node context) throws RepositoryException { + GridLayout layout = CmsSwtUtils.noSpaceGridLayout(); + parent.setLayout(layout); + Composite filterCmp = new Composite(parent, SWT.NO_FOCUS); + filterCmp.setLayoutData(CmsSwtUtils.fillWidth()); + + // top filter + addFilterPanel(filterCmp); + + // scrolled composite + scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER | SWT.NO_FOCUS); + scrolledCmp.setLayoutData(CmsSwtUtils.fillAll()); + scrolledCmp.setExpandVertical(true); + scrolledCmp.setExpandHorizontal(true); + scrolledCmp.setShowFocusedControl(true); + + colViewer = new Composite(scrolledCmp, SWT.NO_FOCUS); + scrolledCmp.setContent(colViewer); + scrolledCmp.addControlListener(new ControlAdapter() { + private static final long serialVersionUID = 6589392045145698201L; + + @Override + public void controlResized(ControlEvent e) { + Rectangle r = scrolledCmp.getClientArea(); + scrolledCmp.setMinSize(colViewer.computeSize(SWT.DEFAULT, r.height)); + } + }); + initExplorer(colViewer, context); + } + + private Control initExplorer(Composite parent, Node context) throws RepositoryException { + parent.setLayout(CmsSwtUtils.noSpaceGridLayout()); + createBrowserColumn(parent, context); + return null; + } + + private Control createBrowserColumn(Composite parent, Node context) throws RepositoryException { + // TODO style is not correctly managed. + FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(parent, SWT.BORDER | SWT.NO_FOCUS, context); + // CmsUiUtils.style(table, ArgeoOrgStyle.browserColumn.style()); + table.filterList("*"); + table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true)); + browserCols.put(context.getPath(), table); + return null; + } + + public void addFilterPanel(Composite parent) { + + parent.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(2, false))); + + // Text Area for the filter + parentPathTxt = new Text(parent, SWT.NO_FOCUS); + parentPathTxt.setEditable(false); + filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL); + filterTxt.setMessage("Filter current list"); + filterTxt.setLayoutData(CmsSwtUtils.fillWidth()); + filterTxt.addModifyListener(new ModifyListener() { + private static final long serialVersionUID = 7709303319740056286L; + + public void modifyText(ModifyEvent event) { + modifyFilter(false); + } + }); + + filterTxt.addKeyListener(new KeyListener() { + private static final long serialVersionUID = -4523394262771183968L; + + @Override + public void keyReleased(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0; + // boolean altPressed = (e.stateMask & SWT.ALT) != 0; + FilterEntitiesVirtualTable currTable = null; + if (currEdited != null) { + FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited)); + if (table != null && !table.isDisposed()) + currTable = table; + } + + try { + if (e.keyCode == SWT.ARROW_DOWN) + currTable.setFocus(); + else if (e.keyCode == SWT.BS) { + if (filterTxt.getText().equals("") + && !(getPath(currEdited).equals("/") || getPath(currEdited).equals(initialPath))) { + setEdited(currEdited.getParent()); + e.doit = false; + filterTxt.setFocus(); + } + } else if (e.keyCode == SWT.TAB && !shiftPressed) { + if (currEdited.getNodes(filterTxt.getText() + "*").getSize() == 1) { + setEdited(currEdited.getNodes(filterTxt.getText() + "*").nextNode()); + } + filterTxt.setFocus(); + e.doit = false; + } + } catch (RepositoryException e1) { + throw new CmsException("Unexpected error in key management for " + currEdited + "with filter " + + filterTxt.getText(), e1); + } + + } + }); + } + + private void setEdited(Node node) { + try { + currEdited = node; + CmsSwtUtils.clear(nodeDisplayParent); + createNodeView(nodeDisplayParent, currEdited); + nodeDisplayParent.layout(); + refreshFilters(node); + refreshBrowser(node); + } catch (RepositoryException re) { + throw new CmsException("Unable to update browser for " + node, re); + } + } + + private void refreshFilters(Node node) throws RepositoryException { + String currNodePath = node.getPath(); + parentPathTxt.setText(currNodePath); + filterTxt.setText(""); + filterTxt.getParent().layout(); + } + + private void refreshBrowser(Node node) throws RepositoryException { + + // Retrieve + String currNodePath = node.getPath(); + String currParPath = ""; + if (!"/".equals(currNodePath)) + currParPath = JcrUtils.parentPath(currNodePath); + if ("".equals(currParPath)) + currParPath = "/"; + + Object[][] colMatrix = new Object[browserCols.size()][2]; + + int i = 0, j = -1, k = -1; + for (String path : browserCols.keySet()) { + colMatrix[i][0] = path; + colMatrix[i][1] = browserCols.get(path); + if (j >= 0 && k < 0 && !currNodePath.equals("/")) { + boolean leaveOpened = path.startsWith(currNodePath); + + // workaround for same name siblings + // fix me weird side effect when we go left or click on anb + // already selected, unfocused node + if (leaveOpened && (path.lastIndexOf("/") == 0 && currNodePath.lastIndexOf("/") == 0 + || JcrUtils.parentPath(path).equals(JcrUtils.parentPath(currNodePath)))) + leaveOpened = JcrUtils.lastPathElement(path).equals(JcrUtils.lastPathElement(currNodePath)); + + if (!leaveOpened) + k = i; + } + if (currParPath.equals(path)) + j = i; + i++; + } + + if (j >= 0 && k >= 0) + // remove useless cols + for (int l = i - 1; l >= k; l--) { + browserCols.remove(colMatrix[l][0]); + ((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose(); + } + + // Remove disposed columns + // TODO investigate and fix the mechanism that leave them there after + // disposal + if (browserCols.containsKey(currNodePath)) { + FilterEntitiesVirtualTable currCol = browserCols.get(currNodePath); + if (currCol.isDisposed()) + browserCols.remove(currNodePath); + } + + if (!browserCols.containsKey(currNodePath)) + createBrowserColumn(colViewer, node); + + colViewer.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(browserCols.size(), false))); + // colViewer.pack(); + colViewer.layout(); + // also resize the scrolled composite + scrolledCmp.layout(); + scrolledCmp.getShowFocusedControl(); + // colViewer.getParent().layout(); + // if (JcrUtils.parentPath(currNodePath).equals(currBrowserKey)) { + // } else { + // } + } + + private void modifyFilter(boolean fromOutside) { + if (!fromOutside) + if (currEdited != null) { + String filter = filterTxt.getText() + "*"; + FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited)); + if (table != null && !table.isDisposed()) + table.filterList(filter); + } + + } + + private String getPath(Node node) { + try { + return node.getPath(); + } catch (RepositoryException e) { + throw new CmsException("Unable to get path for node " + node, e); + } + } + + private Cms2DSize imageWidth = new Cms2DSize(250, 0); + + /** + * Recreates the content of the box that displays information about the current + * selected node. + */ + private Control createNodeView(Composite parent, Node context) throws RepositoryException { + + parent.setLayout(new GridLayout(2, false)); + + if (isImg(context)) { + EditableImage image = new Img(parent, RIGHT, context, imageWidth); + image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 2, 1)); + } + + // Name and primary type + Label contextL = new Label(parent, SWT.NONE); + CmsSwtUtils.markup(contextL); + contextL.setText("" + context.getName() + ""); + new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType().getName()); + + // Children + for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) { + Node child = nIt.nextNode(); + new CmsLink(child.getName(), BROWSE_PREFIX + child.getPath()).createUi(parent, context); + new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType().getName()); + } + + // Properties + for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) { + Property property = pIt.nextProperty(); + Label label = new Label(parent, SWT.NONE); + label.setText(property.getName()); + label.setToolTipText(JcrUtils.getPropertyDefinitionAsString(property)); + new Label(parent, SWT.NONE).setText(getPropAsString(property)); + } + + return null; + } + + private boolean isImg(Node node) throws RepositoryException { + // TODO support images + return false; +// return node.hasNode(JCR_CONTENT) && node.isNodeType(CmsTypes.CMS_IMAGE); + } + + private String getPropAsString(Property property) throws RepositoryException { + String result = ""; + if (property.isMultiple()) { + result = getMultiAsString(property, ", "); + } else { + Value value = property.getValue(); + if (value.getType() == PropertyType.BINARY) + result = ""; + else if (value.getType() == PropertyType.DATE) + result = timeFormatter.format(value.getDate().getTime()); + else + result = value.getString(); + } + return result; + } + + private String getMultiAsString(Property property, String separator) throws RepositoryException { + if (separator == null) + separator = "; "; + Value[] values = property.getValues(); + StringBuilder builder = new StringBuilder(); + for (Value val : values) { + String currStr = val.getString(); + if (!"".equals(currStr.trim())) + builder.append(currStr).append(separator); + } + if (builder.lastIndexOf(separator) >= 0) + return builder.substring(0, builder.length() - separator.length()); + else + return builder.toString(); + } + + /** Almost canonical implementation of a table that display entities */ + private class FilterEntitiesVirtualTable extends Composite { + private static final long serialVersionUID = 8798147431706283824L; + + // Context + private Node context; + + // UI Objects + private TableViewer entityViewer; + + // enable management of multiple columns + Node getNode() { + return context; + } + + @Override + public boolean setFocus() { + if (entityViewer.getTable().isDisposed()) + return false; + if (entityViewer.getSelection().isEmpty()) { + Object first = entityViewer.getElementAt(0); + if (first != null) { + entityViewer.setSelection(new StructuredSelection(first), true); + } + } + return entityViewer.getTable().setFocus(); + } + + void filterList(String filter) { + try { + NodeIterator nit = context.getNodes(filter); + refreshFilteredList(nit); + } catch (RepositoryException e) { + throw new CmsException("Unable to filter " + getNode() + " children with filter " + filter, e); + } + + } + + public FilterEntitiesVirtualTable(Composite parent, int style, Node context) { + super(parent, SWT.NO_FOCUS); + this.context = context; + populate(); + } + + protected void populate() { + Composite parent = this; + GridLayout layout = CmsSwtUtils.noSpaceGridLayout(); + + this.setLayout(layout); + createTableViewer(parent); + } + + private void createTableViewer(final Composite parent) { + // the list + // We must limit the size of the table otherwise the full list is + // loaded + // before the layout happens + Composite listCmp = new Composite(parent, SWT.NO_FOCUS); + GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true); + gd.widthHint = COLUMN_WIDTH; + listCmp.setLayoutData(gd); + listCmp.setLayout(CmsSwtUtils.noSpaceGridLayout()); + + entityViewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.SINGLE); + Table table = entityViewer.getTable(); + + table.setLayoutData(CmsSwtUtils.fillAll()); + table.setLinesVisible(true); + table.setHeaderVisible(false); + CmsSwtUtils.markup(table); + + CmsSwtUtils.style(table, MaintenanceStyles.BROWSER_COLUMN); + + // first column + TableViewerColumn column = new TableViewerColumn(entityViewer, SWT.NONE); + TableColumn tcol = column.getColumn(); + tcol.setWidth(COLUMN_WIDTH); + tcol.setResizable(true); + column.setLabelProvider(new SimpleNameLP()); + + entityViewer.setContentProvider(new MyLazyCP(entityViewer)); + entityViewer.addSelectionChangedListener(new ISelectionChangedListener() { + + @Override + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection(); + if (selection.isEmpty()) + return; + else + setEdited((Node) selection.getFirstElement()); + + } + }); + + table.addKeyListener(new KeyListener() { + private static final long serialVersionUID = -330694313896036230L; + + @Override + public void keyReleased(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + + IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection(); + Node selected = null; + if (!selection.isEmpty()) + selected = ((Node) selection.getFirstElement()); + try { + if (e.keyCode == SWT.ARROW_RIGHT) { + if (selected != null) { + setEdited(selected); + browserCols.get(selected.getPath()).setFocus(); + } + } else if (e.keyCode == SWT.ARROW_LEFT) { + try { + selected = getNode().getParent(); + String newPath = selected.getPath(); // getNode().getParent() + setEdited(selected); + if (browserCols.containsKey(newPath)) + browserCols.get(newPath).setFocus(); + } catch (ItemNotFoundException ie) { + // root silent + } + } + } catch (RepositoryException ie) { + throw new CmsException("Error while managing arrow " + "events in the browser for " + selected, + ie); + } + } + }); + } + + private class MyLazyCP implements ILazyContentProvider { + private static final long serialVersionUID = 1L; + private TableViewer viewer; + private Object[] elements; + + public MyLazyCP(TableViewer viewer) { + this.viewer = viewer; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // IMPORTANT: don't forget this: an exception will be thrown if + // a selected object is not part of the results anymore. + viewer.setSelection(null); + this.elements = (Object[]) newInput; + } + + public void updateElement(int index) { + viewer.replace(elements[index], index); + } + } + + protected void refreshFilteredList(NodeIterator children) { + Object[] rows = JcrUtils.nodeIteratorToList(children).toArray(); + entityViewer.setInput(rows); + entityViewer.setItemCount(rows.length); + entityViewer.refresh(); + } + + public class SimpleNameLP extends ColumnLabelProvider { + private static final long serialVersionUID = 2465059387875338553L; + + @Override + public String getText(Object element) { + if (element instanceof Node) { + Node curr = ((Node) element); + try { + return curr.getName(); + } catch (RepositoryException e) { + throw new CmsException("Unable to get name for" + curr); + } + } + return super.getText(element); + } + } + } +} \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/ConnectivityDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/ConnectivityDeploymentUi.java new file mode 100644 index 000000000..97f3e6713 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/ConnectivityDeploymentUi.java @@ -0,0 +1,48 @@ +package org.argeo.cms.e4.maintenance; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.HttpService; +import org.osgi.service.useradmin.UserAdmin; + +class ConnectivityDeploymentUi extends AbstractOsgiComposite { + private static final long serialVersionUID = 590221539553514693L; + + public ConnectivityDeploymentUi(Composite parent, int style) { + super(parent, style); + } + + @Override + protected void initUi(int style) { + StringBuffer text = new StringBuffer(); + text.append("Provided Servers
"); + + ServiceReference userAdminRef = bc.getServiceReference(HttpService.class); + if (userAdminRef != null) { + // FIXME use constants + Object httpPort = userAdminRef.getProperty("http.port"); + Object httpsPort = userAdminRef.getProperty("https.port"); + if (httpPort != null) + text.append("http ").append(httpPort).append("
"); + if (httpsPort != null) + text.append("https ").append(httpsPort).append("
"); + + } + + text.append("
"); + text.append("Referenced Servers
"); + + Label label = new Label(this, SWT.NONE); + label.setData(new GridData(SWT.FILL, SWT.FILL, false, false)); + CmsSwtUtils.markup(label); + label.setText(text.toString()); + } + + protected boolean isDeployed() { + return bc.getServiceReference(UserAdmin.class) != null; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DataDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DataDeploymentUi.java new file mode 100644 index 000000000..ef95bde64 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DataDeploymentUi.java @@ -0,0 +1,139 @@ +package org.argeo.cms.e4.maintenance; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; + +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.osgi.framework.ServiceReference; + +class DataDeploymentUi extends AbstractOsgiComposite { + private static final long serialVersionUID = 590221539553514693L; + + public DataDeploymentUi(Composite parent, int style) { + super(parent, style); + } + + @Override + protected void initUi(int style) { + if (isDeployed()) { + initCurrentUi(this); + } else { + initNewUi(this); + } + } + + private void initNewUi(Composite parent) { +// try { +// ConfigurationAdmin confAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class)); +// Configuration[] confs = confAdmin.listConfigurations( +// "(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + NodeConstants.NODE_REPOS_FACTORY_PID + ")"); +// if (confs == null || confs.length == 0) { +// Group buttonGroup = new Group(parent, SWT.NONE); +// buttonGroup.setText("Repository Type"); +// buttonGroup.setLayout(new GridLayout(2, true)); +// buttonGroup.setLayoutData(new GridData(GridData.FILL_VERTICAL)); +// +// SelectionListener selectionListener = new SelectionAdapter() { +// private static final long serialVersionUID = 6247064348421088092L; +// +// public void widgetSelected(SelectionEvent event) { +// Button radio = (Button) event.widget; +// if (!radio.getSelection()) +// return; +// log.debug(event); +// JackrabbitType nodeType = (JackrabbitType) radio.getData(); +// if (log.isDebugEnabled()) +// log.debug(" selected = " + nodeType.name()); +// }; +// }; +// +// for (JackrabbitType nodeType : JackrabbitType.values()) { +// Button radio = new Button(buttonGroup, SWT.RADIO); +// radio.setText(nodeType.name()); +// radio.setData(nodeType); +// if (nodeType.equals(JackrabbitType.localfs)) +// radio.setSelection(true); +// radio.addSelectionListener(selectionListener); +// } +// +// } else if (confs.length == 1) { +// +// } else { +// throw new CmsException("Multiple repos not yet supported"); +// } +// } catch (Exception e) { +// throw new CmsException("Cannot initialize UI", e); +// } + + } + + private void initCurrentUi(Composite parent) { + parent.setLayout(new GridLayout()); + Collection> contexts = getServiceReferences(RepositoryContext.class, + "(" + CmsConstants.CN + "=*)"); + StringBuffer text = new StringBuffer(); + text.append("Jackrabbit Repositories
"); + for (ServiceReference sr : contexts) { + RepositoryContext repositoryContext = bc.getService(sr); + String alias = sr.getProperty(CmsConstants.CN).toString(); + String rootNodeId = repositoryContext.getRootNodeId().toString(); + RepositoryConfig repositoryConfig = repositoryContext.getRepositoryConfig(); + Path repoHomePath = new File(repositoryConfig.getHomeDir()).toPath().toAbsolutePath(); + // TODO check data store + + text.append("" + alias + "
"); + text.append("rootNodeId: " + rootNodeId + "
"); + try { + FileStore fileStore = Files.getFileStore(repoHomePath); + text.append("partition: " + fileStore.toString() + "
"); + text.append( + percentUsed(fileStore) + " used (" + humanReadable(fileStore.getUsableSpace()) + " free)
"); + } catch (IOException e) { + log.error("Cannot check fileStore for " + repoHomePath, e); + } + } + Label label = new Label(parent, SWT.NONE); + label.setData(new GridData(SWT.FILL, SWT.FILL, false, false)); + CmsSwtUtils.markup(label); + label.setText("" + text.toString() + ""); + } + + private String humanReadable(long bytes) { + long mb = bytes / (1024 * 1024); + return mb >= 2048 ? Long.toString(mb / 1024) + " GB" : Long.toString(mb) + " MB"; + } + + private String percentUsed(FileStore fs) throws IOException { + long used = fs.getTotalSpace() - fs.getUnallocatedSpace(); + long percent = used * 100 / fs.getTotalSpace(); + if (log.isTraceEnabled()) { + // output identical to `df -B 1`) + log.trace(fs.getTotalSpace() + "," + used + "," + fs.getUsableSpace()); + } + String span; + if (percent < 80) + span = ""; + else if (percent < 95) + span = ""; + else + span = ""; + return span + percent + "%"; + } + + protected boolean isDeployed() { + return bc.getServiceReference(RepositoryContext.class) != null; + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DeploymentEntryPoint.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DeploymentEntryPoint.java new file mode 100644 index 000000000..0a28dc5ee --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/DeploymentEntryPoint.java @@ -0,0 +1,95 @@ +package org.argeo.cms.e4.maintenance; + +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsContext; +import org.argeo.api.cms.CmsDeployment; +import org.argeo.api.cms.CmsState; +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; + +class DeploymentEntryPoint { + private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext(); + + protected void createContents(Composite parent) { + // FIXME manage authentication if needed + // if (!CurrentUser.roles().contains(AuthConstants.ROLE_ADMIN)) + // return; + + // parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + if (isDesktop()) { + parent.setLayout(new GridLayout(2, true)); + } else { + // TODO add scrolling + parent.setLayout(new GridLayout(1, true)); + } + + initHighLevelSummary(parent); + + Group securityGroup = createHighLevelGroup(parent, "Security"); + securityGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + new SecurityDeploymentUi(securityGroup, SWT.NONE); + + Group dataGroup = createHighLevelGroup(parent, "Data"); + dataGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + new DataDeploymentUi(dataGroup, SWT.NONE); + + Group logGroup = createHighLevelGroup(parent, "Notifications"); + logGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true)); + new LogDeploymentUi(logGroup, SWT.NONE); + + Group connectivityGroup = createHighLevelGroup(parent, "Connectivity"); + new ConnectivityDeploymentUi(connectivityGroup, SWT.NONE); + connectivityGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true)); + + } + + private void initHighLevelSummary(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); + if (isDesktop()) + gridData.horizontalSpan = 3; + composite.setLayoutData(gridData); + composite.setLayout(new FillLayout()); + + ServiceReference nodeStateRef = bc.getServiceReference(CmsState.class); + if (nodeStateRef == null) + throw new IllegalStateException("No CMS state available"); + CmsState nodeState = bc.getService(nodeStateRef); + ServiceReference nodeDeploymentRef = bc.getServiceReference(CmsContext.class); + Label label = new Label(composite, SWT.WRAP); + CmsSwtUtils.markup(label); + if (nodeDeploymentRef == null) { + label.setText("Not yet deployed on, please configure below."); + } else { + Object stateUuid = nodeStateRef.getProperty(CmsConstants.CN); + CmsContext nodeDeployment = bc.getService(nodeDeploymentRef); + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(nodeDeployment.getAvailableSince()); + calendar.setTimeZone(TimeZone.getDefault()); + label.setText("Deployment state " + stateUuid + ", available since " + calendar.getTime() + ""); + } + } + + private static Group createHighLevelGroup(Composite parent, String text) { + Group group = new Group(parent, SWT.NONE); + group.setText(text); + CmsSwtUtils.markup(group); + return group; + } + + private boolean isDesktop() { + return true; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/LogDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/LogDeploymentUi.java new file mode 100644 index 000000000..fa5d3dae3 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/LogDeploymentUi.java @@ -0,0 +1,73 @@ +package org.argeo.cms.e4.maintenance; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Enumeration; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogListener; +import org.osgi.service.log.LogReaderService; + +class LogDeploymentUi extends AbstractOsgiComposite implements LogListener { + private static final long serialVersionUID = 590221539553514693L; + + private DateFormat dateFormat = new SimpleDateFormat("MMdd HH:mm"); + + private Display display; + private Text logDisplay; + + public LogDeploymentUi(Composite parent, int style) { + super(parent, style); + } + + @Override + protected void initUi(int style) { + LogReaderService logReader = getService(LogReaderService.class); + // FIXME use server push + // logReader.addLogListener(this); + this.display = getDisplay(); + this.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + logDisplay = new Text(this, SWT.WRAP | SWT.MULTI | SWT.READ_ONLY); + logDisplay.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + CmsSwtUtils.markup(logDisplay); + Enumeration logEntries = (Enumeration) logReader.getLog(); + while (logEntries.hasMoreElements()) + logDisplay.append(printEntry(logEntries.nextElement())); + } + + private String printEntry(LogEntry entry) { + StringBuilder sb = new StringBuilder(); + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getDefault()); + calendar.setTimeInMillis(entry.getTime()); + sb.append(dateFormat.format(calendar.getTime())).append(' '); + sb.append(entry.getMessage()); + sb.append('\n'); + return sb.toString(); + } + + @Override + public void logged(LogEntry entry) { + if (display.isDisposed()) + return; + display.asyncExec(() -> { + if (logDisplay.isDisposed()) + return; + logDisplay.append(printEntry(entry)); + }); + display.wake(); + } + + // @Override + // public void dispose() { + // super.dispose(); + // getService(LogReaderService.class).removeLogListener(this); + // } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/MaintenanceStyles.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/MaintenanceStyles.java new file mode 100644 index 000000000..df1be51ad --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/MaintenanceStyles.java @@ -0,0 +1,10 @@ +package org.argeo.cms.e4.maintenance; + +/** Specific styles used by the various maintenance pages . */ +public interface MaintenanceStyles { + // General + public final static String PREFIX = "maintenance_"; + + // Browser + public final static String BROWSER_COLUMN = "browser_column"; + } diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/NonAdminPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/NonAdminPage.java new file mode 100644 index 000000000..cb38ce899 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/NonAdminPage.java @@ -0,0 +1,30 @@ +package org.argeo.cms.e4.maintenance; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.CmsUiProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +public class NonAdminPage implements CmsUiProvider{ + + @Override + public Control createUi(Composite parent, Node context) + throws RepositoryException { + Composite body = new Composite(parent, SWT.NO_FOCUS); + body.setLayoutData(CmsSwtUtils.fillAll()); + body.setLayout(new GridLayout()); + Label label = new Label(body, SWT.NONE); + label.setText("You should be an admin to perform maintenance operations. " + + "Are you sure you are logged in?"); + label.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + return null; + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/SecurityDeploymentUi.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/SecurityDeploymentUi.java new file mode 100644 index 000000000..3492c5499 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/SecurityDeploymentUi.java @@ -0,0 +1,85 @@ +package org.argeo.cms.e4.maintenance; + +import java.net.URI; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.UserAdmin; + +class SecurityDeploymentUi extends AbstractOsgiComposite { + private static final long serialVersionUID = 590221539553514693L; + + public SecurityDeploymentUi(Composite parent, int style) { + super(parent, style); + } + + @Override + protected void initUi(int style) { + if (isDeployed()) { + initCurrentUi(this); + } else { + initNewUi(this); + } + } + + private void initNewUi(Composite parent) { + new Label(parent, SWT.NONE).setText("Security is not configured"); + } + + private void initCurrentUi(Composite parent) { + ServiceReference userAdminRef = bc.getServiceReference(UserAdmin.class); + UserAdmin userAdmin = bc.getService(userAdminRef); + StringBuffer text = new StringBuffer(); + text.append("Domains
"); + domains: for (String key : userAdminRef.getPropertyKeys()) { + if (!key.startsWith("/")) + continue domains; + URI uri; + try { + uri = new URI(key); + } catch (Exception e) { + // ignore non URI keys + continue domains; + } + + String rootDn = uri.getPath().substring(1, uri.getPath().length()); + // FIXME make reading query options more robust, using utils + boolean readOnly = uri.getQuery().equals("readOnly=true"); + if (readOnly) + text.append(""); + else + text.append(""); + + text.append(rootDn); + text.append("
"); + try { + Role[] roles = userAdmin.getRoles("(dn=*," + rootDn + ")"); + long userCount = 0; + long groupCount = 0; + for (Role role : roles) { + if (role.getType() == Role.USER) + userCount++; + else + groupCount++; + } + text.append(" " + userCount + " users, " + groupCount +" groups.
"); + } catch (InvalidSyntaxException e) { + log.error("Invalid syntax", e); + } + } + Label label = new Label(parent, SWT.NONE); + label.setData(new GridData(SWT.FILL, SWT.FILL, false, false)); + CmsSwtUtils.markup(label); + label.setText(text.toString()); + } + + protected boolean isDeployed() { + return bc.getServiceReference(UserAdmin.class) != null; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/package-info.java new file mode 100644 index 000000000..e4d2ad4c3 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/maintenance/package-info.java @@ -0,0 +1,2 @@ +/** Maintenance perspective. */ +package org.argeo.cms.e4.maintenance; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java new file mode 100644 index 000000000..e9536830f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java @@ -0,0 +1,46 @@ +package org.argeo.cms.e4.monitoring; + +import org.argeo.cms.ux.widgets.TreeParent; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +/** A tree element representing a {@link Bundle} */ +class BundleNode extends TreeParent { + private final Bundle bundle; + + public BundleNode(Bundle bundle) { + this(bundle, false); + } + + @SuppressWarnings("rawtypes") + public BundleNode(Bundle bundle, boolean hasChildren) { + super(bundle.getSymbolicName()); + this.bundle = bundle; + + if (hasChildren) { + // REFERENCES + ServiceReference[] usedServices = bundle.getServicesInUse(); + if (usedServices != null) { + for (ServiceReference sr : usedServices) { + if (sr != null) + addChild(new ServiceReferenceNode(sr, false)); + } + } + + // SERVICES + ServiceReference[] registeredServices = bundle + .getRegisteredServices(); + if (registeredServices != null) { + for (ServiceReference sr : registeredServices) { + if (sr != null) + addChild(new ServiceReferenceNode(sr, true)); + } + } + } + + } + + Bundle getBundle() { + return bundle; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java new file mode 100644 index 000000000..c639255b6 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java @@ -0,0 +1,114 @@ +//package org.argeo.eclipse.ui.workbench.osgi; +//public class BundlesView {} + +package org.argeo.cms.e4.monitoring; + +import javax.annotation.PostConstruct; + +import org.argeo.eclipse.ui.ColumnViewerComparator; +import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** + * Overview of the bundles as a table. Equivalent to Equinox 'ss' console + * command. + */ +public class BundlesView { + private final static BundleContext bc = FrameworkUtil.getBundle(BundlesView.class).getBundleContext(); + private TableViewer viewer; + + @PostConstruct + public void createPartControl(Composite parent) { + viewer = new TableViewer(parent); + viewer.setContentProvider(new BundleContentProvider()); + viewer.getTable().setHeaderVisible(true); + + EclipseUiSpecificUtils.enableToolTipSupport(viewer); + + // ID + TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(30); + column.getColumn().setText("ID"); + column.getColumn().setAlignment(SWT.RIGHT); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -3122136344359358605L; + + public String getText(Object element) { + return Long.toString(((Bundle) element).getBundleId()); + } + }); + new ColumnViewerComparator(column); + + // State + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(18); + column.getColumn().setText("State"); + column.setLabelProvider(new StateLabelProvider()); + new ColumnViewerComparator(column); + + // Symbolic name + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(250); + column.getColumn().setText("Symbolic Name"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -4280840684440451080L; + + public String getText(Object element) { + return ((Bundle) element).getSymbolicName(); + } + }); + new ColumnViewerComparator(column); + + // Version + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(250); + column.getColumn().setText("Version"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = 6871926308708629989L; + + public String getText(Object element) { + Bundle bundle = (org.osgi.framework.Bundle) element; + return bundle.getVersion().toString(); + } + }); + new ColumnViewerComparator(column); + + viewer.setInput(bc); + + } + + @Focus + public void setFocus() { + if (viewer != null) + viewer.getControl().setFocus(); + } + + /** Content provider managing the array of bundles */ + private static class BundleContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = -8533792785725875977L; + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BundleContext) { + BundleContext bc = (BundleContext) inputElement; + return bc.getBundles(); + } + return null; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java new file mode 100644 index 000000000..95b1eb2cb --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java @@ -0,0 +1,173 @@ +//package org.argeo.eclipse.ui.workbench.osgi; +//public class BundlesView {} + +package org.argeo.cms.e4.monitoring; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.argeo.api.cms.CmsSession; +import org.argeo.cms.auth.RoleNameUtils; +import org.argeo.eclipse.ui.ColumnViewerComparator; +import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils; +import org.argeo.util.LangUtils; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * Overview of the active CMS sessions. + */ +public class CmsSessionsView { + private final static BundleContext bc = FrameworkUtil.getBundle(CmsSessionsView.class).getBundleContext(); + + private TableViewer viewer; + + @PostConstruct + public void createPartControl(Composite parent) { + viewer = new TableViewer(parent); + viewer.setContentProvider(new CmsSessionContentProvider()); + viewer.getTable().setHeaderVisible(true); + + EclipseUiSpecificUtils.enableToolTipSupport(viewer); + + int longColWidth = 150; + int smallColWidth = 100; + + // Display name + TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(longColWidth); + column.getColumn().setText("User"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getDisplayName(); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getUserDn().toString(); + } + }); + new ColumnViewerComparator(column); + + // Creation time + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Since"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return LangUtils.since(((CmsSession) element).getCreationTime()); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getCreationTime().toString(); + } + }); + new ColumnViewerComparator(column); + + // Username + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Username"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + String userDn = ((CmsSession) element).getUserDn(); + return RoleNameUtils.getLastRdnValue(userDn); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getUserDn().toString(); + } + }); + new ColumnViewerComparator(column); + + // UUID + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("UUID"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getUuid().toString(); + } + + public String getToolTipText(Object element) { + return getText(element); + } + }); + new ColumnViewerComparator(column); + + // Local ID + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Local ID"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getLocalId(); + } + + public String getToolTipText(Object element) { + return getText(element); + } + }); + new ColumnViewerComparator(column); + + viewer.setInput(bc); + + } + + @Focus + public void setFocus() { + if (viewer != null) + viewer.getControl().setFocus(); + } + + /** Content provider managing the array of bundles */ + private static class CmsSessionContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = -8533792785725875977L; + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BundleContext) { + BundleContext bc = (BundleContext) inputElement; + Collection> srs; + try { + srs = bc.getServiceReferences(CmsSession.class, null); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot retrieve CMS sessions", e); + } + List res = new ArrayList<>(); + for (ServiceReference sr : srs) { + res.add(bc.getService(sr)); + } + return res.toArray(); + } + return null; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java new file mode 100644 index 000000000..6317882c4 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java @@ -0,0 +1,91 @@ +package org.argeo.cms.e4.monitoring; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.argeo.cms.ux.widgets.TreeParent; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** The OSGi runtime from a module perspective. */ +public class ModulesView { + private final static BundleContext bc = FrameworkUtil.getBundle(ModulesView.class).getBundleContext(); + private TreeViewer viewer; + + @PostConstruct + public void createPartControl(Composite parent) { + viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + viewer.setContentProvider(new ModulesContentProvider()); + viewer.setLabelProvider(new ModulesLabelProvider()); + viewer.setInput(bc); + } + + @Focus + public void setFocus() { + viewer.getTree().setFocus(); + } + + private class ModulesContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = 3819934804640641721L; + + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof BundleContext) { + BundleContext bundleContext = (BundleContext) parentElement; + Bundle[] bundles = bundleContext.getBundles(); + + List modules = new ArrayList(); + for (Bundle bundle : bundles) { + if (bundle.getState() == Bundle.ACTIVE) + modules.add(new BundleNode(bundle, true)); + } + return modules.toArray(); + } else if (parentElement instanceof TreeParent) { + return ((TreeParent) parentElement).getChildren(); + } else { + return null; + } + } + + public Object getParent(Object element) { + // TODO Auto-generated method stub + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof TreeParent) { + return ((TreeParent) element).hasChildren(); + } + return false; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } + + private class ModulesLabelProvider extends StateLabelProvider { + private static final long serialVersionUID = 5290046145534824722L; + + @Override + public String getText(Object element) { + if (element instanceof BundleNode) + return element.toString() + " [" + ((BundleNode) element).getBundle().getBundleId() + "]"; + return element.toString(); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java new file mode 100644 index 000000000..5db8bd151 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java @@ -0,0 +1,163 @@ +package org.argeo.cms.e4.monitoring; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.argeo.cms.swt.CmsException; +import org.argeo.util.LangUtils; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class OsgiConfigurationsView { + private final static BundleContext bc = FrameworkUtil.getBundle(OsgiConfigurationsView.class).getBundleContext(); + + @PostConstruct + public void createPartControl(Composite parent) { + ConfigurationAdmin configurationAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class)); + + TreeViewer viewer = new TreeViewer(parent); + // viewer.getTree().setHeaderVisible(true); + + TreeViewerColumn tvc = new TreeViewerColumn(viewer, SWT.NONE); + tvc.getColumn().setWidth(400); + tvc.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = 835407996597566763L; + + @Override + public String getText(Object element) { + if (element instanceof Configuration) { + return ((Configuration) element).getPid(); + } else if (element instanceof Prop) { + return ((Prop) element).key; + } + return super.getText(element); + } + + @Override + public Image getImage(Object element) { + if (element instanceof Configuration) + return OsgiExplorerImages.CONFIGURATION; + return null; + } + + }); + + tvc = new TreeViewerColumn(viewer, SWT.NONE); + tvc.getColumn().setWidth(400); + tvc.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = 6999659261190014687L; + + @Override + public String getText(Object element) { + if (element instanceof Configuration) { + // return ((Configuration) element).getFactoryPid(); + return null; + } else if (element instanceof Prop) { + return ((Prop) element).value.toString(); + } + return super.getText(element); + } + }); + + viewer.setContentProvider(new ConfigurationsContentProvider()); + viewer.setInput(configurationAdmin); + } + + static class ConfigurationsContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = -4892768279440981042L; + private ConfigurationComparator configurationComparator = new ConfigurationComparator(); + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + ConfigurationAdmin configurationAdmin = (ConfigurationAdmin) inputElement; + try { + Configuration[] configurations = configurationAdmin.listConfigurations(null); + Arrays.sort(configurations, configurationComparator); + return configurations; + } catch (IOException | InvalidSyntaxException e) { + throw new CmsException("Cannot list configurations", e); + } + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof Configuration) { + List res = new ArrayList<>(); + Configuration configuration = (Configuration) parentElement; + Dictionary props = configuration.getProperties(); + keys: for (String key : LangUtils.keys(props)) { + if (Constants.SERVICE_PID.equals(key)) + continue keys; + if (ConfigurationAdmin.SERVICE_FACTORYPID.equals(key)) + continue keys; + res.add(new Prop(configuration, key, props.get(key))); + } + return res.toArray(new Prop[res.size()]); + } + return null; + } + + @Override + public Object getParent(Object element) { + if (element instanceof Prop) + return ((Prop) element).configuration; + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof Configuration) + return true; + return false; + } + + } + + static class Prop { + final Configuration configuration; + final String key; + final Object value; + + public Prop(Configuration configuration, String key, Object value) { + this.configuration = configuration; + this.key = key; + this.value = value; + } + + } + + static class ConfigurationComparator implements Comparator { + + @Override + public int compare(Configuration o1, Configuration o2) { + return o1.getPid().compareTo(o2.getPid()); + } + + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java new file mode 100644 index 000000000..7217fe612 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java @@ -0,0 +1,15 @@ +package org.argeo.cms.e4.monitoring; + +import org.argeo.cms.ui.theme.CmsImages; +import org.eclipse.swt.graphics.Image; + +/** Shared icons. */ +public class OsgiExplorerImages extends CmsImages { + public final static Image INSTALLED = createIcon("installed.gif"); + public final static Image RESOLVED = createIcon("resolved.gif"); + public final static Image STARTING = createIcon("starting.gif"); + public final static Image ACTIVE = createIcon("active.gif"); + public final static Image SERVICE_PUBLISHED = createIcon("service_published.gif"); + public final static Image SERVICE_REFERENCED = createIcon("service_referenced.gif"); + public final static Image CONFIGURATION = createIcon("node.gif"); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java new file mode 100644 index 000000000..1c60811d2 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java @@ -0,0 +1,46 @@ +package org.argeo.cms.e4.monitoring; + +import org.argeo.cms.ux.widgets.TreeParent; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +/** A tree element representing a {@link ServiceReference} */ +@SuppressWarnings({ "rawtypes" }) +class ServiceReferenceNode extends TreeParent { + private final ServiceReference serviceReference; + private final boolean published; + + public ServiceReferenceNode(ServiceReference serviceReference, + boolean published) { + super(serviceReference.toString()); + this.serviceReference = serviceReference; + this.published = published; + + if (isPublished()) { + Bundle[] usedBundles = serviceReference.getUsingBundles(); + if (usedBundles != null) { + for (Bundle b : usedBundles) { + if (b != null) + addChild(new BundleNode(b)); + } + } + } else { + Bundle provider = serviceReference.getBundle(); + addChild(new BundleNode(provider)); + } + + for (String key : serviceReference.getPropertyKeys()) { + addChild(new TreeParent(key + "=" + + serviceReference.getProperty(key))); + } + + } + + public ServiceReference getServiceReference() { + return serviceReference; + } + + public boolean isPublished() { + return published; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java new file mode 100644 index 000000000..5cb5b6563 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java @@ -0,0 +1,82 @@ +package org.argeo.cms.e4.monitoring; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; + +/** Label provider showing the sate of bundles */ +class StateLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -7885583135316000733L; + + @Override + public Image getImage(Object element) { + int state; + if (element instanceof Bundle) + state = ((Bundle) element).getState(); + else if (element instanceof BundleNode) + state = ((BundleNode) element).getBundle().getState(); + else if (element instanceof ServiceReferenceNode) + if (((ServiceReferenceNode) element).isPublished()) + return OsgiExplorerImages.SERVICE_PUBLISHED; + else + return OsgiExplorerImages.SERVICE_REFERENCED; + else + return null; + + switch (state) { + case Bundle.UNINSTALLED: + return OsgiExplorerImages.INSTALLED; + case Bundle.INSTALLED: + return OsgiExplorerImages.INSTALLED; + case Bundle.RESOLVED: + return OsgiExplorerImages.RESOLVED; + case Bundle.STARTING: + return OsgiExplorerImages.STARTING; + case Bundle.STOPPING: + return OsgiExplorerImages.STARTING; + case Bundle.ACTIVE: + return OsgiExplorerImages.ACTIVE; + default: + return null; + } + } + + @Override + public String getText(Object element) { + return null; + } + + @Override + public String getToolTipText(Object element) { + Bundle bundle = (Bundle) element; + Integer state = bundle.getState(); + switch (state) { + case Bundle.UNINSTALLED: + return "UNINSTALLED"; + case Bundle.INSTALLED: + return "INSTALLED"; + case Bundle.RESOLVED: + return "RESOLVED"; + case Bundle.STARTING: + String activationPolicy = bundle.getHeaders() + .get(Constants.BUNDLE_ACTIVATIONPOLICY).toString(); + + // .get("Bundle-ActivationPolicy").toString(); + // FIXME constant triggers the compilation failure + if (activationPolicy != null + && activationPolicy.equals(Constants.ACTIVATION_LAZY)) + // && activationPolicy.equals("lazy")) + // FIXME constant triggers the compilation failure + // && activationPolicy.equals(Constants.ACTIVATION_LAZY)) + return "<>"; + return "STARTING"; + case Bundle.STOPPING: + return "STOPPING"; + case Bundle.ACTIVE: + return "ACTIVE"; + default: + return null; + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/package-info.java new file mode 100644 index 000000000..873bf3118 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/monitoring/package-info.java @@ -0,0 +1,2 @@ +/** Monitoring perspective. */ +package org.argeo.cms.e4.monitoring; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/parts/EgoDashboard.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/parts/EgoDashboard.java new file mode 100644 index 000000000..f2a73f210 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/parts/EgoDashboard.java @@ -0,0 +1,41 @@ +package org.argeo.cms.e4.parts; + +import java.time.ZonedDateTime; + +import javax.annotation.PostConstruct; + +import org.argeo.api.cms.CmsSession; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; + +/** A canonical view of the logged in user. */ +public class EgoDashboard { +// private BundleContext bc = FrameworkUtil.getBundle(EgoDashboard.class).getBundleContext(); + + @PostConstruct + public void createPartControl(Composite p) { + p.setLayout(new GridLayout()); + String username = CurrentUser.getUsername(); + + CmsSwtUtils.lbl(p, "" + CurrentUser.getDisplayName() + ""); + CmsSwtUtils.txt(p, username); + CmsSwtUtils.lbl(p, "Roles:"); + roles: for (String role : CurrentUser.roles()) { + if (username.equals(role)) + continue roles; + CmsSwtUtils.txt(p, role); + } + +// Subject subject = Subject.getSubject(AccessController.getContext()); +// if (subject != null) { + CmsSession cmsSession = CurrentUser.getCmsSession(); + ZonedDateTime loggedIndSince = cmsSession.getCreationTime(); + CmsSwtUtils.lbl(p, "Session:"); + CmsSwtUtils.txt(p, cmsSession.getUuid().toString()); + CmsSwtUtils.lbl(p, "Logged in since:"); + CmsSwtUtils.txt(p, loggedIndSince.toString()); +// } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java new file mode 100644 index 000000000..137f76242 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java @@ -0,0 +1,287 @@ +package org.argeo.cms.e4.users; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.ui.eclipse.forms.AbstractFormPart; +import org.argeo.cms.ui.eclipse.forms.IManagedForm; +import org.argeo.cms.ui.eclipse.forms.ManagedForm; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.e4.ui.di.Persist; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Editor for a user, might be a user or a group. */ +public abstract class AbstractRoleEditor { + + // public final static String USER_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID + + // ".userEditor"; + // public final static String GROUP_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID + + // ".groupEditor"; + + /* DEPENDENCY INJECTION */ + @Inject + protected UserAdminWrapper userAdminWrapper; + + @Inject + private MPart mPart; + + // @Inject + // Composite parent; + + private UserAdmin userAdmin; + + // Context + private User user; + private String username; + + private NameChangeListener listener; + + private ManagedForm managedForm; + + // public void init(IEditorSite site, IEditorInput input) throws + // PartInitException { + @PostConstruct + public void init(Composite parent) { + this.userAdmin = userAdminWrapper.getUserAdmin(); + username = mPart.getPersistedState().get(LdapAttrs.uid.name()); + user = (User) userAdmin.getRole(username); + + listener = new NameChangeListener(Display.getCurrent()); + userAdminWrapper.addListener(listener); + updateEditorTitle(null); + + managedForm = new ManagedForm(parent) { + + @Override + public void staleStateChanged() { + refresh(); + } + }; + ScrolledComposite scrolled = managedForm.getForm(); + Composite body = new Composite(scrolled, SWT.NONE); + scrolled.setContent(body); + createUi(body); + managedForm.refresh(); + } + + abstract void createUi(Composite parent); + + /** + * returns the list of all authorizations for the given user or of the current + * displayed user if parameter is null + */ + protected List getFlatGroups(User aUser) { + Authorization currAuth; + if (aUser == null) + currAuth = userAdmin.getAuthorization(this.user); + else + currAuth = userAdmin.getAuthorization(aUser); + + String[] roles = currAuth.getRoles(); + + List groups = new ArrayList(); + for (String roleStr : roles) { + User currRole = (User) userAdmin.getRole(roleStr); + if (currRole != null && !groups.contains(currRole)) + groups.add(currRole); + } + return groups; + } + + protected IManagedForm getManagedForm() { + return managedForm; + } + + /** Exposes the user (or group) that is displayed by the current editor */ + protected User getDisplayedUser() { + return user; + } + + private void setDisplayedUser(User user) { + this.user = user; + } + + void updateEditorTitle(String title) { + if (title == null) { + String commonName = UserAdminUtils.getProperty(user, LdapAttrs.cn.name()); + title = "".equals(commonName) ? user.getName() : commonName; + } + setPartName(title); + } + + protected void setPartName(String name) { + mPart.setLabel(name); + } + + // protected void addPages() { + // try { + // if (user.getType() == Role.GROUP) + // addPage(new GroupMainPage(this, userAdminWrapper, repository, nodeInstance)); + // else + // addPage(new UserMainPage(this, userAdminWrapper)); + // } catch (Exception e) { + // throw new CmsException("Cannot add pages", e); + // } + // } + + @Persist + public void doSave(IProgressMonitor monitor) { + userAdminWrapper.beginTransactionIfNeeded(); + commitPages(true); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + // firePropertyChange(PROP_DIRTY); + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user)); + } + + protected void commitPages(boolean b) { + managedForm.commit(b); + } + + @PreDestroy + public void dispose() { + userAdminWrapper.removeListener(listener); + managedForm.dispose(); + } + + // CONTROLERS FOR THIS EDITOR AND ITS PAGES + + class NameChangeListener extends UiUserAdminListener { + public NameChangeListener(Display display) { + super(display); + } + + @Override + public void roleChangedToUiThread(UserAdminEvent event) { + Role changedRole = event.getRole(); + if (changedRole == null || changedRole.equals(user)) { + updateEditorTitle(null); + User reloadedUser = (User) userAdminWrapper.getUserAdmin().getRole(user.getName()); + setDisplayedUser(reloadedUser); + } + } + } + + class MainInfoListener extends UiUserAdminListener { + private final AbstractFormPart part; + + public MainInfoListener(Display display, AbstractFormPart part) { + super(display); + this.part = part; + } + + @Override + public void roleChangedToUiThread(UserAdminEvent event) { + // Rollback + if (event.getRole() == null) + part.markStale(); + } + } + + class GroupChangeListener extends UiUserAdminListener { + private final AbstractFormPart part; + + public GroupChangeListener(Display display, AbstractFormPart part) { + super(display); + this.part = part; + } + + @Override + public void roleChangedToUiThread(UserAdminEvent event) { + // always mark as stale + part.markStale(); + } + } + + /** Registers a listener that will notify this part */ + class FormPartML implements ModifyListener { + private static final long serialVersionUID = 6299808129505381333L; + private AbstractFormPart formPart; + + public FormPartML(AbstractFormPart generalPart) { + this.formPart = generalPart; + } + + public void modifyText(ModifyEvent e) { + // Discard event when the control does not have the focus, typically + // to avoid all editors being marked as dirty during a Rollback + if (((Control) e.widget).isFocusControl()) + formPart.markDirty(); + } + } + + /* DEPENDENCY INJECTION */ + public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) { + this.userAdminWrapper = userAdminWrapper; + } + + /** Creates label and multiline text. */ + Text createLMT(Composite parent, String label, String value) { + Label lbl = new Label(parent, SWT.NONE); + lbl.setText(label); + lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false)); + Text text = new Text(parent, SWT.NONE); + text.setText(value); + text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, true)); + return text; + } + + /** Creates label and password. */ + Text createLP(Composite parent, String label, String value) { + Label lbl = new Label(parent, SWT.NONE); + lbl.setText(label); + lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false)); + Text text = new Text(parent, SWT.PASSWORD | SWT.BORDER); + text.setText(value); + text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, false)); + return text; + } + + /** Creates label and text. */ + Text createLT(Composite parent, String label, String value) { + Label lbl = new Label(parent, SWT.NONE); + lbl.setText(label); + lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false)); + lbl.setFont(EclipseUiUtils.getBoldFont(parent)); + Text text = new Text(parent, SWT.BORDER); + text.setText(value); + text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT); + return text; + } + + Text createReadOnlyLT(Composite parent, String label, String value) { + Label lbl = new Label(parent, SWT.NONE); + lbl.setText(label); + lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false)); + lbl.setFont(EclipseUiUtils.getBoldFont(parent)); + Text text = new Text(parent, SWT.NONE); + text.setText(value); + text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, false)); + text.setEditable(false); + // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT); + return text; + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java new file mode 100644 index 000000000..07df312e1 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java @@ -0,0 +1,8 @@ +package org.argeo.cms.e4.users; + +/** Centralize the declaration of Workbench specific CSS Styles */ +interface CmsWorkbenchStyles { + + // Specific People layouting + String WORKBENCH_FORM_TEXT = "workbench_form_text"; +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupEditor.java new file mode 100644 index 000000000..80c5998e3 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupEditor.java @@ -0,0 +1,566 @@ +package org.argeo.cms.e4.users; + +import static org.argeo.api.cms.CmsContext.WORKGROUP; +import static org.argeo.cms.auth.UserAdminUtils.setProperty; +import static org.argeo.util.naming.LdapAttrs.businessCategory; +import static org.argeo.util.naming.LdapAttrs.description; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsContext; +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.providers.CommonNameLP; +import org.argeo.cms.e4.users.providers.MailLP; +import org.argeo.cms.e4.users.providers.RoleIconLP; +import org.argeo.cms.e4.users.providers.UserFilter; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.useradmin.LdifUsersTable; +import org.argeo.cms.ui.eclipse.forms.AbstractFormPart; +import org.argeo.cms.ui.eclipse.forms.IManagedForm; +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.transaction.WorkTransaction; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.ViewerDropAdapter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.DropTargetEvent; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.dnd.TransferData; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +//import org.eclipse.ui.forms.AbstractFormPart; +//import org.eclipse.ui.forms.IManagedForm; +//import org.eclipse.ui.forms.SectionPart; +//import org.eclipse.ui.forms.editor.FormEditor; +//import org.eclipse.ui.forms.editor.FormPage; +//import org.eclipse.ui.forms.widgets.FormToolkit; +//import org.eclipse.ui.forms.widgets.ScrolledForm; +//import org.eclipse.ui.forms.widgets.Section; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Display/edit main properties of a given group */ +public class GroupEditor extends AbstractRoleEditor { + // final static String ID = "GroupEditor.mainPage"; + + @Inject + private EPartService partService; + + // private final UserEditor editor; + @Inject + private Repository repository; + @Inject + private CmsContext nodeInstance; + // private final UserAdminWrapper userAdminWrapper; + private Session groupsSession; + + // public GroupMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper, + // Repository repository, + // NodeInstance nodeInstance) { + // super(editor, ID, "Main"); + // try { + // session = repository.login(); + // } catch (RepositoryException e) { + // throw new CmsException("Cannot retrieve session of in MainGroupPage + // constructor", e); + // } + // this.editor = (UserEditor) editor; + // this.userAdminWrapper = userAdminWrapper; + // this.nodeInstance = nodeInstance; + // } + + // protected void createFormContent(final IManagedForm mf) { + // ScrolledForm form = mf.getForm(); + // Composite body = form.getBody(); + // GridLayout mainLayout = new GridLayout(); + // body.setLayout(mainLayout); + // Group group = (Group) editor.getDisplayedUser(); + // appendOverviewPart(body, group); + // appendMembersPart(body, group); + // } + + @Override + protected void createUi(Composite parent) { + try { + groupsSession = repository.login(CmsConstants.SRV_WORKSPACE); + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve session", e); + } + // ScrolledForm form = mf.getForm(); + // Composite body = form.getBody(); + // Composite body = new Composite(parent, SWT.NONE); + Composite body = parent; + GridLayout mainLayout = new GridLayout(); + body.setLayout(mainLayout); + Group group = (Group) getDisplayedUser(); + appendOverviewPart(body, group); + appendMembersPart(body, group); + } + + @PreDestroy + public void dispose() { + JcrUtils.logoutQuietly(groupsSession); + super.dispose(); + } + + /** Creates the general section */ + protected void appendOverviewPart(final Composite parent, final Group group) { + Composite body = new Composite(parent, SWT.NONE); + // GridLayout layout = new GridLayout(5, false); + GridLayout layout = new GridLayout(2, false); + body.setLayout(layout); + body.setLayoutData(CmsSwtUtils.fillWidth()); + + String cn = UserAdminUtils.getProperty(group, LdapAttrs.cn.name()); + createReadOnlyLT(body, "Name", cn); + createReadOnlyLT(body, "DN", group.getName()); + createReadOnlyLT(body, "Domain", UserAdminUtils.getDomainName(group)); + + // Description + Label descLbl = new Label(body, SWT.LEAD); + descLbl.setFont(EclipseUiUtils.getBoldFont(body)); + descLbl.setText("Description"); + descLbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, true, false, 2, 1)); + final Text descTxt = new Text(body, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER); + GridData gd = EclipseUiUtils.fillWidth(); + gd.heightHint = 50; + gd.horizontalSpan = 2; + descTxt.setLayoutData(gd); + + // Mark as workgroup + Link markAsWorkgroupLk = new Link(body, SWT.NONE); + markAsWorkgroupLk.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1)); + + // create form part (controller) + final AbstractFormPart part = new AbstractFormPart() { + + private MainInfoListener listener; + + @Override + public void initialize(IManagedForm form) { + super.initialize(form); + listener = new MainInfoListener(parent.getDisplay(), this); + userAdminWrapper.addListener(listener); + } + + @Override + public void dispose() { + userAdminWrapper.removeListener(listener); + super.dispose(); + } + + public void commit(boolean onSave) { + // group.getProperties().put(LdapAttrs.description.name(), descTxt.getText()); + setProperty(group, description, descTxt.getText()); + super.commit(onSave); + } + + @Override + public void refresh() { + // dnTxt.setText(group.getName()); + // cnTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.cn.name())); + descTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.description.name())); + Node workgroupHome = CmsJcrUtils.getGroupHome(groupsSession, cn); + if (workgroupHome == null) + markAsWorkgroupLk.setText("Mark as workgroup"); + else + markAsWorkgroupLk.setText("Configured as workgroup"); + parent.layout(true, true); + super.refresh(); + } + }; + + markAsWorkgroupLk.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = -6439340898096365078L; + + @Override + public void widgetSelected(SelectionEvent e) { + + boolean confirmed = MessageDialog.openConfirm(parent.getShell(), "Mark as workgroup", + "Are you sure you want to mark " + cn + " as being a workgroup? "); + if (confirmed) { + Node workgroupHome = CmsJcrUtils.getGroupHome(groupsSession, cn); + if (workgroupHome != null) + return; // already marked as workgroup, do nothing + else { + // improve transaction management + userAdminWrapper.beginTransactionIfNeeded(); + nodeInstance.createWorkgroup(group.getName()); + setProperty(group, businessCategory, WORKGROUP); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group)); + part.refresh(); + } + } + } + }); + + ModifyListener defaultListener = new FormPartML(part); + descTxt.addModifyListener(defaultListener); + getManagedForm().addPart(part); + } + + /** Filtered table with members. Has drag and drop ability */ + protected void appendMembersPart(Composite parent, Group group) { + // Section section = tk.createSection(parent, Section.TITLE_BAR); + // section.setText("Members"); + // section.setLayoutData(EclipseUiUtils.fillAll()); + + Composite body = new Composite(parent, SWT.BORDER); + body.setLayout(new GridLayout()); + // section.setClient(body); + body.setLayoutData(EclipseUiUtils.fillAll()); + + // Define the displayed columns + List columnDefs = new ArrayList(); + columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24)); + columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150)); + columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150)); + // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", + // 240)); + + // Create and configure the table + LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL, + userAdminWrapper.getUserAdmin()); + + userViewerCmp.setColumnDefinitions(columnDefs); + userViewerCmp.populate(true, false); + userViewerCmp.setLayoutData(EclipseUiUtils.fillAll()); + + // Controllers + TableViewer userViewer = userViewerCmp.getTableViewer(); + userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService)); + int operations = DND.DROP_COPY | DND.DROP_MOVE; + Transfer[] tt = new Transfer[] { TextTransfer.getInstance() }; + userViewer.addDropSupport(operations, tt, + new GroupDropListener(userAdminWrapper, userViewerCmp, (Group) getDisplayedUser())); + + AbstractFormPart part = new GroupMembersPart(userViewerCmp); + getManagedForm().addPart(part); + + // remove button + // addRemoveAbility(toolBarManager, userViewerCmp.getTableViewer(), group); + Action action = new RemoveMembershipAction(userViewer, group, "Remove selected items from this group", + SecurityAdminImages.ICON_REMOVE_DESC); + + ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + ToolBar toolBar = toolBarManager.createControl(body); + toolBar.setLayoutData(CmsSwtUtils.fillWidth()); + + toolBarManager.add(action); + toolBarManager.update(true); + + } + + // private LdifUsersTable createMemberPart(Composite parent, Group group) { + // + // // Define the displayed columns + // List columnDefs = new ArrayList(); + // columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24)); + // columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150)); + // columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150)); + // // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished + // Name", + // // 240)); + // + // // Create and configure the table + // LdifUsersTable userViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | + // SWT.H_SCROLL | SWT.V_SCROLL, + // userAdminWrapper.getUserAdmin()); + // + // userViewerCmp.setColumnDefinitions(columnDefs); + // userViewerCmp.populate(true, false); + // userViewerCmp.setLayoutData(EclipseUiUtils.fillAll()); + // + // // Controllers + // TableViewer userViewer = userViewerCmp.getTableViewer(); + // userViewer.addDoubleClickListener(new + // UserTableDefaultDClickListener(partService)); + // int operations = DND.DROP_COPY | DND.DROP_MOVE; + // Transfer[] tt = new Transfer[] { TextTransfer.getInstance() }; + // userViewer.addDropSupport(operations, tt, + // new GroupDropListener(userAdminWrapper, userViewerCmp, (Group) + // getDisplayedUser())); + // + // // userViewerCmp.refresh(); + // return userViewerCmp; + // } + + // Local viewers + private class MyUserTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 8467999509931900367L; + + private final UserFilter userFilter; + + public MyUserTableViewer(Composite parent, int style, UserAdmin userAdmin) { + super(parent, style, true); + userFilter = new UserFilter(); + + } + + @Override + protected List listFilteredElements(String filter) { + // reload user and set it in the editor + Group group = (Group) getDisplayedUser(); + Role[] roles = group.getMembers(); + List users = new ArrayList(); + userFilter.setSearchText(filter); + // userFilter.setShowSystemRole(true); + for (Role role : roles) + // if (role.getType() == Role.GROUP) + if (userFilter.select(null, null, role)) + users.add((User) role); + return users; + } + } + + // private void addRemoveAbility(ToolBarManager toolBarManager, TableViewer + // userViewer, Group group) { + // // Section section = sectionPart.getSection(); + // // ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + // // ToolBar toolbar = toolBarManager.createControl(parent); + // // ToolBar toolbar = toolBarManager.getControl(); + // // final Cursor handCursor = new Cursor(toolbar.getDisplay(), + // SWT.CURSOR_HAND); + // // toolbar.setCursor(handCursor); + // // toolbar.addDisposeListener(new DisposeListener() { + // // private static final long serialVersionUID = 3882131405820522925L; + // // + // // public void widgetDisposed(DisposeEvent e) { + // // if ((handCursor != null) && (handCursor.isDisposed() == false)) { + // // handCursor.dispose(); + // // } + // // } + // // }); + // + // Action action = new RemoveMembershipAction(userViewer, group, "Remove + // selected items from this group", + // SecurityAdminImages.ICON_REMOVE_DESC); + // toolBarManager.add(action); + // toolBarManager.update(true); + // // section.setTextClient(toolbar); + // } + + private class RemoveMembershipAction extends Action { + private static final long serialVersionUID = -1337713097184522588L; + + private final TableViewer userViewer; + private final Group group; + + RemoveMembershipAction(TableViewer userViewer, Group group, String name, ImageDescriptor img) { + super(name, img); + this.userViewer = userViewer; + this.group = group; + } + + @Override + public void run() { + ISelection selection = userViewer.getSelection(); + if (selection.isEmpty()) + return; + + @SuppressWarnings("unchecked") + Iterator it = ((IStructuredSelection) selection).iterator(); + List users = new ArrayList(); + while (it.hasNext()) { + User currUser = it.next(); + users.add(currUser); + } + + userAdminWrapper.beginTransactionIfNeeded(); + for (User user : users) { + group.removeMember(user); + } + userAdminWrapper.commitOrNotifyTransactionStateChange(); + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group)); + } + } + + // LOCAL CONTROLLERS + private class GroupMembersPart extends AbstractFormPart { + private final LdifUsersTable userViewer; + // private final Group group; + + private GroupChangeListener listener; + + public GroupMembersPart(LdifUsersTable userViewer) { + // super(section); + this.userViewer = userViewer; + // this.group = group; + } + + @Override + public void initialize(IManagedForm form) { + super.initialize(form); + listener = new GroupChangeListener(userViewer.getDisplay(), GroupMembersPart.this); + userAdminWrapper.addListener(listener); + } + + @Override + public void dispose() { + userAdminWrapper.removeListener(listener); + super.dispose(); + } + + @Override + public void refresh() { + userViewer.refresh(); + super.refresh(); + } + } + + /** + * Defines this table as being a potential target to add group membership + * (roles) to this group + */ + private class GroupDropListener extends ViewerDropAdapter { + private static final long serialVersionUID = 2893468717831451621L; + + private final UserAdminWrapper userAdminWrapper; + // private final LdifUsersTable myUserViewerCmp; + private final Group myGroup; + + public GroupDropListener(UserAdminWrapper userAdminWrapper, LdifUsersTable userTableViewerCmp, Group group) { + super(userTableViewerCmp.getTableViewer()); + this.userAdminWrapper = userAdminWrapper; + this.myGroup = group; + // this.myUserViewerCmp = userTableViewerCmp; + } + + @Override + public boolean validateDrop(Object target, int operation, TransferData transferType) { + // Target is always OK in a list only view + // TODO check if not a string + boolean validDrop = true; + return validDrop; + } + + @Override + public void drop(DropTargetEvent event) { + // TODO Is there an opportunity to perform the check before? + String newUserName = (String) event.data; + UserAdmin myUserAdmin = userAdminWrapper.getUserAdmin(); + Role role = myUserAdmin.getRole(newUserName); + if (role.getType() == Role.GROUP) { + Group newGroup = (Group) role; + Shell shell = getViewer().getControl().getShell(); + // Sanity checks + if (myGroup == newGroup) { // Equality + MessageDialog.openError(shell, "Forbidden addition ", "A group cannot be a member of itself."); + return; + } + + // Cycle + String myName = myGroup.getName(); + List myMemberships = getFlatGroups(myGroup); + if (myMemberships.contains(newGroup)) { + MessageDialog.openError(shell, "Forbidden addition: cycle", + "Cannot add " + newUserName + " to group " + myName + ". This would create a cycle"); + return; + } + + // Already member + List newGroupMemberships = getFlatGroups(newGroup); + if (newGroupMemberships.contains(myGroup)) { + MessageDialog.openError(shell, "Forbidden addition", + "Cannot add " + newUserName + " to group " + myName + ", this membership already exists"); + return; + } + userAdminWrapper.beginTransactionIfNeeded(); + myGroup.addMember(newGroup); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup)); + } else if (role.getType() == Role.USER) { + // TODO check if the group is already member of this group + WorkTransaction transaction = userAdminWrapper.beginTransactionIfNeeded(); + User user = (User) role; + myGroup.addMember(user); + if (UserAdminWrapper.COMMIT_ON_SAVE) + try { + transaction.commit(); + } catch (Exception e) { + throw new IllegalStateException( + "Cannot commit transaction " + "after user group membership update", e); + } + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup)); + } + super.drop(event); + } + + @Override + public boolean performDrop(Object data) { + // myUserViewerCmp.refresh(); + return true; + } + } + + // LOCAL HELPERS + // private Composite addSection(FormToolkit tk, Composite parent) { + // Section section = tk.createSection(parent, SWT.NO_FOCUS); + // section.setLayoutData(EclipseUiUtils.fillWidth()); + // Composite body = tk.createComposite(section, SWT.WRAP); + // body.setLayoutData(EclipseUiUtils.fillAll()); + // section.setClient(body); + // return body; + // } + + /** Creates label and text. */ + // private Text createLT(Composite parent, String label, String value) { + // FormToolkit toolkit = getManagedForm().getToolkit(); + // Label lbl = toolkit.createLabel(parent, label); + // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false)); + // lbl.setFont(EclipseUiUtils.getBoldFont(parent)); + // Text text = toolkit.createText(parent, value, SWT.BORDER); + // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT); + // return text; + // } + // + // Text createReadOnlyLT(Composite parent, String label, String value) { + // FormToolkit toolkit = getManagedForm().getToolkit(); + // Label lbl = toolkit.createLabel(parent, label); + // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false)); + // lbl.setFont(EclipseUiUtils.getBoldFont(parent)); + // Text text = toolkit.createText(parent, value, SWT.NONE); + // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + // text.setEditable(false); + // CmsUiUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT); + // return text; + // } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupsView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupsView.java new file mode 100644 index 000000000..84a279e78 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/GroupsView.java @@ -0,0 +1,251 @@ +package org.argeo.cms.e4.users; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.e4.users.providers.CommonNameLP; +import org.argeo.cms.e4.users.providers.DomainNameLP; +import org.argeo.cms.e4.users.providers.RoleIconLP; +import org.argeo.cms.e4.users.providers.UserDragListener; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.useradmin.LdifUsersTable; +//import org.argeo.cms.ui.workbench.WorkbenchUiPlugin; +//import org.argeo.cms.ui.workbench.internal.useradmin.UiUserAdminListener; +//import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper; +//import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP; +//import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP; +//import org.argeo.cms.ui.workbench.internal.useradmin.providers.RoleIconLP; +//import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserDragListener; +//import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener; +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +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.Display; +//import org.eclipse.ui.part.ViewPart; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdminEvent; +import org.osgi.service.useradmin.UserAdminListener; + +/** List all groups with filter */ +public class GroupsView { + private final static CmsLog log = CmsLog.getLog(GroupsView.class); + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".groupsView"; + + @Inject + private EPartService partService; + @Inject + private UserAdminWrapper userAdminWrapper; + + // UI Objects + private LdifUsersTable groupTableViewerCmp; + private TableViewer userViewer; + private List columnDefs = new ArrayList(); + + private UserAdminListener listener; + + @PostConstruct + public void createPartControl(Composite parent, ESelectionService selectionService) { + parent.setLayout(EclipseUiUtils.noSpaceGridLayout()); + + // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN); + + // Define the displayed columns + columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 19)); + columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150)); + columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100)); + // Only show technical DN to admin + // if (isAdmin) + // columnDefs.add(new ColumnDefinition(new UserNameLP(), + // "Distinguished Name", 300)); + + // Create and configure the table + groupTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + + groupTableViewerCmp.setColumnDefinitions(columnDefs); + // if (isAdmin) + // groupTableViewerCmp.populateWithStaticFilters(false, false); + // else + groupTableViewerCmp.populate(true, false); + + groupTableViewerCmp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + // Links + userViewer = groupTableViewerCmp.getTableViewer(); + userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService)); + // getViewSite().setSelectionProvider(userViewer); + userViewer.addSelectionChangedListener(new ISelectionChangedListener() { + + @Override + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + selectionService.setSelection(selection.toList()); + } + }); + + // Really? + groupTableViewerCmp.refresh(); + + // Drag and drop + int operations = DND.DROP_COPY | DND.DROP_MOVE; + Transfer[] tt = new Transfer[] { TextTransfer.getInstance() }; + userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer)); + + // // Register a useradmin listener + // listener = new UserAdminListener() { + // @Override + // public void roleChanged(UserAdminEvent event) { + // if (userViewer != null && !userViewer.getTable().isDisposed()) + // refresh(); + // } + // }; + // userAdminWrapper.addListener(listener); + // } + + // Register a useradmin listener + listener = new MyUiUAListener(parent.getDisplay()); + userAdminWrapper.addListener(listener); + } + + private class MyUiUAListener extends UiUserAdminListener { + public MyUiUAListener(Display display) { + super(display); + } + + @Override + public void roleChangedToUiThread(UserAdminEvent event) { + if (userViewer != null && !userViewer.getTable().isDisposed()) + refresh(); + } + } + + private class MyUserTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 8467999509931900367L; + + private boolean showSystemRoles = true; + + private final String[] knownProps = { LdapAttrs.uid.name(), LdapAttrs.cn.name(), LdapAttrs.DN }; + + public MyUserTableViewer(Composite parent, int style) { + super(parent, style); + showSystemRoles = CurrentUser.isInRole(CmsConstants.ROLE_ADMIN); + } + + protected void populateStaticFilters(Composite staticFilterCmp) { + staticFilterCmp.setLayout(new GridLayout()); + final Button showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK); + showSystemRoleBtn.setText("Show system roles"); + showSystemRoles = CurrentUser.isInRole(CmsConstants.ROLE_ADMIN); + showSystemRoleBtn.setSelection(showSystemRoles); + + showSystemRoleBtn.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = -7033424592697691676L; + + @Override + public void widgetSelected(SelectionEvent e) { + showSystemRoles = showSystemRoleBtn.getSelection(); + refresh(); + } + + }); + } + + @Override + protected List listFilteredElements(String filter) { + Role[] roles; + try { + StringBuilder builder = new StringBuilder(); + StringBuilder tmpBuilder = new StringBuilder(); + if (EclipseUiUtils.notEmpty(filter)) + for (String prop : knownProps) { + tmpBuilder.append("("); + tmpBuilder.append(prop); + tmpBuilder.append("=*"); + tmpBuilder.append(filter); + tmpBuilder.append("*)"); + } + if (tmpBuilder.length() > 1) { + builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.groupOfNames.name()).append(")"); + // hide tokens + builder.append("(!(").append(LdapAttrs.DN).append("=*").append(CmsConstants.TOKENS_BASEDN) + .append("))"); + + if (!showSystemRoles) + builder.append("(!(").append(LdapAttrs.DN).append("=*").append(CmsConstants.SYSTEM_ROLES_BASEDN) + .append("))"); + builder.append("(|"); + builder.append(tmpBuilder.toString()); + builder.append("))"); + } else { + if (!showSystemRoles) + builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.groupOfNames.name()).append(")(!(").append(LdapAttrs.DN).append("=*") + .append(CmsConstants.SYSTEM_ROLES_BASEDN).append("))(!(").append(LdapAttrs.DN).append("=*") + .append(CmsConstants.TOKENS_BASEDN).append(")))"); + else + builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.groupOfNames.name()).append(")(!(").append(LdapAttrs.DN).append("=*") + .append(CmsConstants.TOKENS_BASEDN).append(")))"); + + } + roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString()); + } catch (InvalidSyntaxException e) { + throw new CmsException("Unable to get roles with filter: " + filter, e); + } + List users = new ArrayList(); + for (Role role : roles) + if (!users.contains(role)) + users.add((User) role); + else + log.warn("Duplicated role: " + role); + + return users; + } + } + + public void refresh() { + groupTableViewerCmp.refresh(); + } + + @PreDestroy + public void dispose() { + userAdminWrapper.removeListener(listener); + } + + @Focus + public void setFocus() { + groupTableViewerCmp.setFocus(); + } + + /* DEPENDENCY INJECTION */ + public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) { + this.userAdminWrapper = userAdminWrapper; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java new file mode 100644 index 000000000..7bbe3c727 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java @@ -0,0 +1,19 @@ +package org.argeo.cms.e4.users; + +import org.argeo.cms.ui.theme.CmsImages; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; + +/** Shared icons that must be declared programmatically . */ +public class SecurityAdminImages extends CmsImages { + private final static String PREFIX = "icons/"; + + public final static ImageDescriptor ICON_REMOVE_DESC = createDesc(PREFIX + "delete.png"); + public final static ImageDescriptor ICON_USER_DESC = createDesc(PREFIX + "person.png"); + + public final static Image ICON_USER = ICON_USER_DESC.createImage(); + public final static Image ICON_GROUP = createImg(PREFIX + "group.png"); + public final static Image ICON_WORKGROUP = createImg(PREFIX + "workgroup.png"); + public final static Image ICON_ROLE = createImg(PREFIX + "role.gif"); + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java new file mode 100644 index 000000000..fb48a47c3 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java @@ -0,0 +1,34 @@ +package org.argeo.cms.e4.users; + +import org.argeo.util.transaction.WorkTransaction; + +/** First effort to centralize back end methods used by the user admin UI */ +public class UiAdminUtils { + /* + * INTERNAL METHODS: Below methods are meant to stay here and are not part + * of a potential generic backend to manage the useradmin + */ + /** Easily notify the ActiveWindow that the transaction had a state change */ + public final static void notifyTransactionStateChange( + WorkTransaction userTransaction) { +// try { +// IWorkbenchWindow aww = PlatformUI.getWorkbench() +// .getActiveWorkbenchWindow(); +// ISourceProviderService sourceProviderService = (ISourceProviderService) aww +// .getService(ISourceProviderService.class); +// UserTransactionProvider esp = (UserTransactionProvider) sourceProviderService +// .getSourceProvider(UserTransactionProvider.TRANSACTION_STATE); +// esp.fireTransactionStateChange(); +// } catch (Exception e) { +// throw new CmsException("Unable to begin transaction", e); +// } + } + + /** + * Email addresses must match this regexp pattern ({@value #EMAIL_PATTERN}. + * Thanks to this tip. + */ + public final static String EMAIL_PATTERN = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java new file mode 100644 index 000000000..eb64aba0e --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java @@ -0,0 +1,27 @@ +package org.argeo.cms.e4.users; + +import org.eclipse.swt.widgets.Display; +import org.osgi.service.useradmin.UserAdminEvent; +import org.osgi.service.useradmin.UserAdminListener; + +/** Convenience class to insure the call to refresh is done in the UI thread */ +public abstract class UiUserAdminListener implements UserAdminListener { + + private final Display display; + + public UiUserAdminListener(Display display) { + this.display = display; + } + + @Override + public void roleChanged(final UserAdminEvent event) { + display.asyncExec(new Runnable() { + @Override + public void run() { + roleChangedToUiThread(event); + } + }); + } + + public abstract void roleChangedToUiThread(UserAdminEvent event); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java new file mode 100644 index 000000000..dbb629c25 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java @@ -0,0 +1,153 @@ +package org.argeo.cms.e4.users; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.swt.CmsException; +import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.transaction.WorkTransaction; +import org.osgi.service.useradmin.UserAdmin; +import org.osgi.service.useradmin.UserAdminEvent; +import org.osgi.service.useradmin.UserAdminListener; + +/** Centralise interaction with the UserAdmin in this bundle */ +public class UserAdminWrapper { + + private UserAdmin userAdmin; + // private ServiceReference userAdminServiceReference; +// private Set uris; + private Map> userDirectories = Collections + .synchronizedMap(new LinkedHashMap<>()); + private WorkTransaction userTransaction; + + // First effort to simplify UX while managing users and groups + public final static boolean COMMIT_ON_SAVE = true; + + // Registered listeners + List listeners = new ArrayList(); + + /** + * Starts a transaction if necessary. Should always been called together with + * {@link UserAdminWrapper#commitOrNotifyTransactionStateChange()} once the + * security model changes have been performed. + */ + public WorkTransaction beginTransactionIfNeeded() { + try { + // UserTransaction userTransaction = getUserTransaction(); + if (userTransaction.isNoTransactionStatus()) { + userTransaction.begin(); + // UiAdminUtils.notifyTransactionStateChange(userTransaction); + } + return userTransaction; + } catch (Exception e) { + throw new CmsException("Unable to begin transaction", e); + } + } + + /** + * Depending on the current application configuration, it will either commit the + * current transaction or throw a notification that the transaction state has + * changed (In the later case, it must be called from the UI thread). + */ + public void commitOrNotifyTransactionStateChange() { + try { + // UserTransaction userTransaction = getUserTransaction(); + if (userTransaction.isNoTransactionStatus()) + return; + + if (UserAdminWrapper.COMMIT_ON_SAVE) + userTransaction.commit(); + else + UiAdminUtils.notifyTransactionStateChange(userTransaction); + } catch (Exception e) { + throw new CmsException("Unable to clean transaction", e); + } + } + + // TODO implement safer mechanism + public void addListener(UserAdminListener userAdminListener) { + if (!listeners.contains(userAdminListener)) + listeners.add(userAdminListener); + } + + public void removeListener(UserAdminListener userAdminListener) { + if (listeners.contains(userAdminListener)) + listeners.remove(userAdminListener); + } + + public void notifyListeners(UserAdminEvent event) { + for (UserAdminListener listener : listeners) + listener.roleChanged(event); + } + + public Map getKnownBaseDns(boolean onlyWritable) { + Map dns = new HashMap(); + for (UserDirectory userDirectory : userDirectories.keySet()) { + Boolean readOnly = userDirectory.isReadOnly(); + String baseDn = userDirectory.getBase(); + + if (onlyWritable && readOnly) + continue; + if (baseDn.equalsIgnoreCase(CmsConstants.SYSTEM_ROLES_BASEDN)) + continue; + if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN)) + continue; + dns.put(baseDn, DirectoryConf.propertiesAsUri(userDirectories.get(userDirectory)).toString()); + + } +// for (String uri : uris) { +// if (!uri.startsWith("/")) +// continue; +// Dictionary props = UserAdminConf.uriAsProperties(uri); +// String readOnly = UserAdminConf.readOnly.getValue(props); +// String baseDn = UserAdminConf.baseDn.getValue(props); +// +// if (onlyWritable && "true".equals(readOnly)) +// continue; +// if (baseDn.equalsIgnoreCase(NodeConstants.ROLES_BASEDN)) +// continue; +// if (baseDn.equalsIgnoreCase(NodeConstants.TOKENS_BASEDN)) +// continue; +// dns.put(baseDn, uri); +// } + return dns; + } + + public UserAdmin getUserAdmin() { + return userAdmin; + } + + public WorkTransaction getUserTransaction() { + return userTransaction; + } + + /* DEPENDENCY INJECTION */ + public void setUserAdmin(UserAdmin userAdmin, Map properties) { + this.userAdmin = userAdmin; +// this.uris = Collections.unmodifiableSortedSet(new TreeSet<>(properties.keySet())); + } + + public void setUserTransaction(WorkTransaction userTransaction) { + this.userTransaction = userTransaction; + } + + public void addUserDirectory(UserDirectory userDirectory, Map properties) { + userDirectories.put(userDirectory, new Hashtable<>(properties)); + } + + public void removeUserDirectory(UserDirectory userDirectory, Map properties) { + userDirectories.remove(userDirectory); + } + + // public void setUserAdminServiceReference( + // ServiceReference userAdminServiceReference) { + // this.userAdminServiceReference = userAdminServiceReference; + // } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java new file mode 100644 index 000000000..6f075f157 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java @@ -0,0 +1,606 @@ +package org.argeo.cms.e4.users; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.providers.CommonNameLP; +import org.argeo.cms.e4.users.providers.DomainNameLP; +import org.argeo.cms.e4.users.providers.MailLP; +import org.argeo.cms.e4.users.providers.UserNameLP; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.useradmin.LdifUsersTable; +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; +import org.argeo.util.transaction.WorkTransaction; +import org.eclipse.jface.dialogs.IPageChangeProvider; +import org.eclipse.jface.dialogs.IPageChangedListener; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.PageChangedEvent; +import org.eclipse.jface.wizard.IWizardContainer; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Wizard to update users */ +public class UserBatchUpdateWizard extends Wizard { + + private final static CmsLog log = CmsLog.getLog(UserBatchUpdateWizard.class); + private UserAdminWrapper userAdminWrapper; + + // pages + private ChooseCommandWizardPage chooseCommandPage; + private ChooseUsersWizardPage userListPage; + private ValidateAndLaunchWizardPage validatePage; + + // Various implemented commands keys + private final static String CMD_UPDATE_PASSWORD = "resetPassword"; + private final static String CMD_UPDATE_EMAIL = "resetEmail"; + private final static String CMD_GROUP_MEMBERSHIP = "groupMembership"; + + private final Map commands = new HashMap() { + private static final long serialVersionUID = 1L; + { + put("Reset password(s)", CMD_UPDATE_PASSWORD); + put("Reset email(s)", CMD_UPDATE_EMAIL); + // TODO implement role / group management + // put("Add/Remove from group", CMD_GROUP_MEMBERSHIP); + } + }; + + public UserBatchUpdateWizard(UserAdminWrapper userAdminWrapper) { + this.userAdminWrapper = userAdminWrapper; + } + + @Override + public void addPages() { + chooseCommandPage = new ChooseCommandWizardPage(); + addPage(chooseCommandPage); + userListPage = new ChooseUsersWizardPage(); + addPage(userListPage); + validatePage = new ValidateAndLaunchWizardPage(); + addPage(validatePage); + } + + @Override + public boolean performFinish() { + if (!canFinish()) + return false; + WorkTransaction ut = userAdminWrapper.getUserTransaction(); + if (!ut.isNoTransactionStatus() && !MessageDialog.openConfirm(getShell(), "Existing Transaction", + "A user transaction is already existing, " + "are you sure you want to proceed ?")) + return false; + + // We cannot use jobs, user modifications are still meant to be done in + // the UIThread + // UpdateJob job = null; + // if (job != null) + // job.schedule(); + + if (CMD_UPDATE_PASSWORD.equals(chooseCommandPage.getCommand())) { + char[] newValue = chooseCommandPage.getPwdValue(); + if (newValue == null) + throw new CmsException("Password cannot be null or an empty string"); + ResetPassword job = new ResetPassword(userAdminWrapper, userListPage.getSelectedUsers(), newValue); + job.doUpdate(); + } else if (CMD_UPDATE_EMAIL.equals(chooseCommandPage.getCommand())) { + String newValue = chooseCommandPage.getEmailValue(); + if (newValue == null) + throw new CmsException("Password cannot be null or an empty string"); + ResetEmail job = new ResetEmail(userAdminWrapper, userListPage.getSelectedUsers(), newValue); + job.doUpdate(); + } + return true; + } + + public boolean canFinish() { + if (this.getContainer().getCurrentPage() == validatePage) + return true; + return false; + } + + private class ResetPassword { + private char[] newPwd; + private UserAdminWrapper userAdminWrapper; + private List usersToUpdate; + + public ResetPassword(UserAdminWrapper userAdminWrapper, List usersToUpdate, char[] newPwd) { + this.newPwd = newPwd; + this.usersToUpdate = usersToUpdate; + this.userAdminWrapper = userAdminWrapper; + } + + @SuppressWarnings("unchecked") + protected void doUpdate() { + userAdminWrapper.beginTransactionIfNeeded(); + try { + for (User user : usersToUpdate) { + // the char array is emptied after being used. + user.getCredentials().put(null, newPwd.clone()); + } + userAdminWrapper.commitOrNotifyTransactionStateChange(); + } catch (Exception e) { + throw new CmsException("Cannot perform batch update on users", e); + } finally { + WorkTransaction ut = userAdminWrapper.getUserTransaction(); + if (!ut.isNoTransactionStatus()) + ut.rollback(); + } + } + } + + private class ResetEmail { + private String newEmail; + private UserAdminWrapper userAdminWrapper; + private List usersToUpdate; + + public ResetEmail(UserAdminWrapper userAdminWrapper, List usersToUpdate, String newEmail) { + this.newEmail = newEmail; + this.usersToUpdate = usersToUpdate; + this.userAdminWrapper = userAdminWrapper; + } + + @SuppressWarnings("unchecked") + protected void doUpdate() { + userAdminWrapper.beginTransactionIfNeeded(); + try { + for (User user : usersToUpdate) { + // the char array is emptied after being used. + user.getProperties().put(LdapAttrs.mail.name(), newEmail); + } + + userAdminWrapper.commitOrNotifyTransactionStateChange(); + if (!usersToUpdate.isEmpty()) + userAdminWrapper.notifyListeners( + new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, usersToUpdate.get(0))); + } catch (Exception e) { + throw new CmsException("Cannot perform batch update on users", e); + } finally { + WorkTransaction ut = userAdminWrapper.getUserTransaction(); + if (!ut.isNoTransactionStatus()) + ut.rollback(); + } + } + } + + // @SuppressWarnings("unused") + // private class AddToGroup extends UpdateJob { + // private String groupID; + // private Session session; + // + // public AddToGroup(Session session, List nodesToUpdate, + // String groupID) { + // super(session, nodesToUpdate); + // this.session = session; + // this.groupID = groupID; + // } + // + // protected void doUpdate(Node node) { + // log.info("Add/Remove to group actions are not yet implemented"); + // // TODO implement this + // // try { + // // throw new CmsException("Not yet implemented"); + // // } catch (RepositoryException re) { + // // throw new CmsException( + // // "Unable to update boolean value for node " + node, re); + // // } + // } + // } + + // /** + // * Base privileged job that will be run asynchronously to perform the + // batch + // * update + // */ + // private abstract class UpdateJob extends PrivilegedJob { + // + // private final UserAdminWrapper userAdminWrapper; + // private final List usersToUpdate; + // + // protected abstract void doUpdate(User user); + // + // public UpdateJob(UserAdminWrapper userAdminWrapper, + // List usersToUpdate) { + // super("Perform update"); + // this.usersToUpdate = usersToUpdate; + // this.userAdminWrapper = userAdminWrapper; + // } + // + // @Override + // protected IStatus doRun(IProgressMonitor progressMonitor) { + // try { + // JcrMonitor monitor = new EclipseJcrMonitor(progressMonitor); + // int total = usersToUpdate.size(); + // monitor.beginTask("Performing change", total); + // userAdminWrapper.beginTransactionIfNeeded(); + // for (User user : usersToUpdate) { + // doUpdate(user); + // monitor.worked(1); + // } + // userAdminWrapper.getUserTransaction().commit(); + // } catch (Exception e) { + // throw new CmsException( + // "Cannot perform batch update on users", e); + // } finally { + // UserTransaction ut = userAdminWrapper.getUserTransaction(); + // try { + // if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION) + // ut.rollback(); + // } catch (IllegalStateException | SecurityException + // | SystemException e) { + // log.error("Unable to rollback session in 'finally', " + // + "the system might be in a dirty state"); + // e.printStackTrace(); + // } + // } + // return Status.OK_STATUS; + // } + // } + + // PAGES + /** + * Displays a combo box that enables user to choose which action to perform + */ + private class ChooseCommandWizardPage extends WizardPage { + private static final long serialVersionUID = -8069434295293996633L; + private Combo chooseCommandCmb; + private Button trueChk; + private Text valueTxt; + private Text pwdTxt; + private Text pwd2Txt; + + public ChooseCommandWizardPage() { + super("Choose a command to run."); + setTitle("Choose a command to run."); + } + + @Override + public void createControl(Composite parent) { + GridLayout gl = new GridLayout(); + Composite container = new Composite(parent, SWT.NO_FOCUS); + container.setLayout(gl); + + chooseCommandCmb = new Combo(container, SWT.READ_ONLY); + chooseCommandCmb.setLayoutData(EclipseUiUtils.fillWidth()); + String[] values = commands.keySet().toArray(new String[0]); + chooseCommandCmb.setItems(values); + + final Composite bottomPart = new Composite(container, SWT.NO_FOCUS); + bottomPart.setLayoutData(EclipseUiUtils.fillAll()); + bottomPart.setLayout(EclipseUiUtils.noSpaceGridLayout()); + + chooseCommandCmb.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = 1L; + + @Override + public void widgetSelected(SelectionEvent e) { + if (getCommand().equals(CMD_UPDATE_PASSWORD)) + populatePasswordCmp(bottomPart); + else if (getCommand().equals(CMD_UPDATE_EMAIL)) + populateEmailCmp(bottomPart); + else if (getCommand().equals(CMD_GROUP_MEMBERSHIP)) + populateGroupCmp(bottomPart); + else + populateBooleanFlagCmp(bottomPart); + checkPageComplete(); + bottomPart.layout(true, true); + } + }); + setControl(container); + } + + private void populateBooleanFlagCmp(Composite parent) { + EclipseUiUtils.clear(parent); + trueChk = new Button(parent, SWT.CHECK); + trueChk.setText("Do it. (It will to the contrary if unchecked)"); + trueChk.setSelection(true); + trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + } + + private void populatePasswordCmp(Composite parent) { + EclipseUiUtils.clear(parent); + Composite body = new Composite(parent, SWT.NO_FOCUS); + + ModifyListener ml = new ModifyListener() { + private static final long serialVersionUID = -1558726363536729634L; + + @Override + public void modifyText(ModifyEvent event) { + checkPageComplete(); + } + }; + + body.setLayout(new GridLayout(2, false)); + body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + pwdTxt = EclipseUiUtils.createGridLP(body, "New password", ml); + pwd2Txt = EclipseUiUtils.createGridLP(body, "Repeat password", ml); + } + + private void populateEmailCmp(Composite parent) { + EclipseUiUtils.clear(parent); + Composite body = new Composite(parent, SWT.NO_FOCUS); + + ModifyListener ml = new ModifyListener() { + private static final long serialVersionUID = 2147704227294268317L; + + @Override + public void modifyText(ModifyEvent event) { + checkPageComplete(); + } + }; + + body.setLayout(new GridLayout(2, false)); + body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + valueTxt = EclipseUiUtils.createGridLT(body, "New e-mail", ml); + } + + private void checkPageComplete() { + String errorMsg = null; + if (chooseCommandCmb.getSelectionIndex() < 0) + errorMsg = "Please select an action"; + else if (CMD_UPDATE_EMAIL.equals(getCommand())) { + if (!valueTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN)) + errorMsg = "Not a valid e-mail address"; + } else if (CMD_UPDATE_PASSWORD.equals(getCommand())) { + if (EclipseUiUtils.isEmpty(pwdTxt.getText()) || pwdTxt.getText().length() < 4) + errorMsg = "Please enter a password that is at least 4 character long"; + else if (!pwdTxt.getText().equals(pwd2Txt.getText())) + errorMsg = "Passwords are different"; + } + if (EclipseUiUtils.notEmpty(errorMsg)) { + setMessage(errorMsg, WizardPage.ERROR); + setPageComplete(false); + } else { + setMessage("Page complete, you can proceed to user choice", WizardPage.INFORMATION); + setPageComplete(true); + } + + getContainer().updateButtons(); + } + + private void populateGroupCmp(Composite parent) { + EclipseUiUtils.clear(parent); + trueChk = new Button(parent, SWT.CHECK); + trueChk.setText("Add to group. (It will remove user(s) from the " + "corresponding group if unchecked)"); + trueChk.setSelection(true); + trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + } + + protected String getCommand() { + return commands.get(chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex())); + } + + protected String getCommandLbl() { + return chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex()); + } + + @SuppressWarnings("unused") + protected boolean getBoleanValue() { + // FIXME this is not consistent and will lead to errors. + if ("argeo:enabled".equals(getCommand())) + return trueChk.getSelection(); + else + return !trueChk.getSelection(); + } + + @SuppressWarnings("unused") + protected String getStringValue() { + String value = null; + if (valueTxt != null) { + value = valueTxt.getText(); + if ("".equals(value.trim())) + value = null; + } + return value; + } + + protected char[] getPwdValue() { + // We do not directly reset the password text fields: There is no + // need to over secure this process: setting a pwd to multi users + // at the same time is anyhow a bad practice and should be used only + // in test environment or for temporary access + if (pwdTxt == null || pwdTxt.isDisposed()) + return null; + else + return pwdTxt.getText().toCharArray(); + } + + protected String getEmailValue() { + // We do not directly reset the password text fields: There is no + // need to over secure this process: setting a pwd to multi users + // at the same time is anyhow a bad practice and should be used only + // in test environment or for temporary access + if (valueTxt == null || valueTxt.isDisposed()) + return null; + else + return valueTxt.getText(); + } + } + + /** + * Displays a list of users with a check box to be able to choose some of them + */ + private class ChooseUsersWizardPage extends WizardPage implements IPageChangedListener { + private static final long serialVersionUID = 7651807402211214274L; + private ChooseUserTableViewer userTableCmp; + + public ChooseUsersWizardPage() { + super("Choose Users"); + setTitle("Select users who will be impacted"); + } + + @Override + public void createControl(Composite parent) { + Composite pageCmp = new Composite(parent, SWT.NONE); + pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout()); + + // Define the displayed columns + List columnDefs = new ArrayList(); + columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150)); + columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150)); + columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200)); + + // Only show technical DN to admin + if (CurrentUser.isInRole(CmsConstants.ROLE_ADMIN)) + columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300)); + + userTableCmp = new ChooseUserTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + userTableCmp.setLayoutData(EclipseUiUtils.fillAll()); + userTableCmp.setColumnDefinitions(columnDefs); + userTableCmp.populate(true, true); + userTableCmp.refresh(); + + setControl(pageCmp); + + // Add listener to update message when shown + final IWizardContainer wContainer = this.getContainer(); + if (wContainer instanceof IPageChangeProvider) { + ((IPageChangeProvider) wContainer).addPageChangedListener(this); + } + + } + + @Override + public void pageChanged(PageChangedEvent event) { + if (event.getSelectedPage() == this) { + String msg = "Chosen batch action: " + chooseCommandPage.getCommandLbl(); + ((WizardPage) event.getSelectedPage()).setMessage(msg); + } + } + + protected List getSelectedUsers() { + return userTableCmp.getSelectedUsers(); + } + + private class ChooseUserTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 5080437561015853124L; + private final String[] knownProps = { LdapAttrs.uid.name(), LdapAttrs.DN, LdapAttrs.cn.name(), + LdapAttrs.givenName.name(), LdapAttrs.sn.name(), LdapAttrs.mail.name() }; + + public ChooseUserTableViewer(Composite parent, int style) { + super(parent, style); + } + + @Override + protected List listFilteredElements(String filter) { + Role[] roles; + + try { + StringBuilder builder = new StringBuilder(); + + StringBuilder tmpBuilder = new StringBuilder(); + if (EclipseUiUtils.notEmpty(filter)) + for (String prop : knownProps) { + tmpBuilder.append("("); + tmpBuilder.append(prop); + tmpBuilder.append("=*"); + tmpBuilder.append(filter); + tmpBuilder.append("*)"); + } + if (tmpBuilder.length() > 1) { + builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.inetOrgPerson.name()).append(")(|"); + builder.append(tmpBuilder.toString()); + builder.append("))"); + } else + builder.append("(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.inetOrgPerson.name()).append(")"); + roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString()); + } catch (InvalidSyntaxException e) { + throw new CmsException("Unable to get roles with filter: " + filter, e); + } + List users = new ArrayList(); + for (Role role : roles) + // Prevent current logged in user to perform batch on + // himself + if (!UserAdminUtils.isCurrentUser((User) role)) + users.add((User) role); + return users; + } + } + } + + /** Summary of input data before launching the process */ + private class ValidateAndLaunchWizardPage extends WizardPage implements IPageChangedListener { + private static final long serialVersionUID = 7098918351451743853L; + private ChosenUsersTableViewer userTableCmp; + + public ValidateAndLaunchWizardPage() { + super("Validate and launch"); + setTitle("Validate and launch"); + } + + @Override + public void createControl(Composite parent) { + Composite pageCmp = new Composite(parent, SWT.NO_FOCUS); + pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout()); + + List columnDefs = new ArrayList(); + columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150)); + columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150)); + columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200)); + // Only show technical DN to admin + if (CurrentUser.isInRole(CmsConstants.ROLE_ADMIN)) + columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300)); + userTableCmp = new ChosenUsersTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + userTableCmp.setLayoutData(EclipseUiUtils.fillAll()); + userTableCmp.setColumnDefinitions(columnDefs); + userTableCmp.populate(false, false); + userTableCmp.refresh(); + setControl(pageCmp); + // Add listener to update message when shown + final IWizardContainer wContainer = this.getContainer(); + if (wContainer instanceof IPageChangeProvider) { + ((IPageChangeProvider) wContainer).addPageChangedListener(this); + } + } + + @Override + public void pageChanged(PageChangedEvent event) { + if (event.getSelectedPage() == this) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + Object[] values = ((ArrayList) userListPage.getSelectedUsers()) + .toArray(new Object[userListPage.getSelectedUsers().size()]); + userTableCmp.getTableViewer().setInput(values); + String msg = "Following batch action: [" + chooseCommandPage.getCommandLbl() + + "] will be perfomed on the users listed below.\n"; + // + "Are you sure you want to proceed?"; + setMessage(msg); + } + } + + private class ChosenUsersTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 7814764735794270541L; + + public ChosenUsersTableViewer(Composite parent, int style) { + super(parent, style); + } + + @Override + protected List listFilteredElements(String filter) { + return userListPage.getSelectedUsers(); + } + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserEditor.java new file mode 100644 index 000000000..fb443e384 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserEditor.java @@ -0,0 +1,535 @@ +package org.argeo.cms.e4.users; + +import static org.argeo.cms.auth.UserAdminUtils.getProperty; +import static org.argeo.util.naming.LdapAttrs.cn; +import static org.argeo.util.naming.LdapAttrs.givenName; +import static org.argeo.util.naming.LdapAttrs.mail; +import static org.argeo.util.naming.LdapAttrs.sn; +import static org.argeo.util.naming.LdapAttrs.uid; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.inject.Inject; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.providers.CommonNameLP; +import org.argeo.cms.e4.users.providers.DomainNameLP; +import org.argeo.cms.e4.users.providers.RoleIconLP; +import org.argeo.cms.e4.users.providers.UserFilter; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.useradmin.LdifUsersTable; +import org.argeo.cms.ui.eclipse.forms.AbstractFormPart; +//import org.argeo.cms.ui.eclipse.forms.FormToolkit; +import org.argeo.cms.ui.eclipse.forms.IManagedForm; +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.TrayDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerDropAdapter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.DropTargetEvent; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.dnd.TransferData; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +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.Display; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Display/edit the properties of a given user */ +public class UserEditor extends AbstractRoleEditor { + // final static String ID = "UserEditor.mainPage"; + + @Inject + private EPartService partService; + + // private final UserEditor editor; + // private UserAdminWrapper userAdminWrapper; + + // Local configuration + // private final int PRE_TITLE_INDENT = 10; + + // public UserMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper) { + // super(editor, ID, "Main"); + // this.editor = (UserEditor) editor; + // this.userAdminWrapper = userAdminWrapper; + // } + + // protected void createFormContent(final IManagedForm mf) { + // ScrolledForm form = mf.getForm(); + // Composite body = form.getBody(); + // GridLayout mainLayout = new GridLayout(); + // // mainLayout.marginRight = 10; + // body.setLayout(mainLayout); + // User user = editor.getDisplayedUser(); + // appendOverviewPart(body, user); + // // Remove to ability to force the password for his own user. The user + // // must then use the change pwd feature + // appendMemberOfPart(body, user); + // } + + @Override + protected void createUi(Composite body) { + // Composite body = new Composite(parent, SWT.BORDER); + GridLayout mainLayout = new GridLayout(); + // mainLayout.marginRight = 10; + body.setLayout(mainLayout); + // body.getParent().setLayout(new GridLayout()); + // body.setLayoutData(CmsUiUtils.fillAll()); + User user = getDisplayedUser(); + appendOverviewPart(body, user); + // Remove to ability to force the password for his own user. The user + // must then use the change pwd feature + appendMemberOfPart(body, user); + } + + /** Creates the general section */ + private void appendOverviewPart(final Composite parent, final User user) { + // FormToolkit tk = getManagedForm().getToolkit(); + + // Section section = tk.createSection(parent, SWT.NO_FOCUS); + // GridData gd = EclipseUiUtils.fillWidth(); + // // gd.verticalAlignment = PRE_TITLE_INDENT; + // section.setLayoutData(gd); + Composite body = new Composite(parent, SWT.NONE); + body.setLayoutData(EclipseUiUtils.fillWidth()); + // section.setClient(body); + // body.setLayout(new GridLayout(6, false)); + body.setLayout(new GridLayout(2, false)); + + Text commonName = createReadOnlyLT(body, "Name", getProperty(user, cn)); + Text distinguishedName = createReadOnlyLT(body, "Login", getProperty(user, uid)); + Text firstName = createLT(body, "First name", getProperty(user, givenName)); + Text lastName = createLT(body, "Last name", getProperty(user, sn)); + Text email = createLT(body, "Email", getProperty(user, mail)); + + Link resetPwdLk = new Link(body, SWT.NONE); + if (!UserAdminUtils.isCurrentUser(user)) { + resetPwdLk.setText("Reset password"); + } + resetPwdLk.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1)); + + // create form part (controller) + AbstractFormPart part = new AbstractFormPart() { + private MainInfoListener listener; + + @Override + public void initialize(IManagedForm form) { + super.initialize(form); + listener = new MainInfoListener(parent.getDisplay(), this); + userAdminWrapper.addListener(listener); + } + + @Override + public void dispose() { + userAdminWrapper.removeListener(listener); + super.dispose(); + } + + @SuppressWarnings("unchecked") + public void commit(boolean onSave) { + // TODO Sanity checks (mail validity...) + user.getProperties().put(LdapAttrs.givenName.name(), firstName.getText()); + user.getProperties().put(LdapAttrs.sn.name(), lastName.getText()); + user.getProperties().put(LdapAttrs.cn.name(), commonName.getText()); + user.getProperties().put(LdapAttrs.mail.name(), email.getText()); + super.commit(onSave); + } + + @Override + public void refresh() { + distinguishedName.setText(UserAdminUtils.getProperty(user, LdapAttrs.uid.name())); + commonName.setText(UserAdminUtils.getProperty(user, LdapAttrs.cn.name())); + firstName.setText(UserAdminUtils.getProperty(user, LdapAttrs.givenName.name())); + lastName.setText(UserAdminUtils.getProperty(user, LdapAttrs.sn.name())); + email.setText(UserAdminUtils.getProperty(user, LdapAttrs.mail.name())); + refreshFormTitle(user); + super.refresh(); + } + }; + + // Improve this: automatically generate CN when first or last name + // changes + ModifyListener cnML = new ModifyListener() { + private static final long serialVersionUID = 4298649222869835486L; + + @Override + public void modifyText(ModifyEvent event) { + String first = firstName.getText(); + String last = lastName.getText(); + String cn = first.trim() + " " + last.trim() + " "; + cn = cn.trim(); + commonName.setText(cn); + // getManagedForm().getForm().setText(cn); + updateEditorTitle(cn); + } + }; + firstName.addModifyListener(cnML); + lastName.addModifyListener(cnML); + + ModifyListener defaultListener = new FormPartML(part); + firstName.addModifyListener(defaultListener); + lastName.addModifyListener(defaultListener); + email.addModifyListener(defaultListener); + + if (!UserAdminUtils.isCurrentUser(user)) + resetPwdLk.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = 5881800534589073787L; + + @Override + public void widgetSelected(SelectionEvent e) { + new ChangePasswordDialog(user, "Reset password").open(); + } + }); + + getManagedForm().addPart(part); + } + + private class ChangePasswordDialog extends TrayDialog { + private static final long serialVersionUID = 2843538207460082349L; + + private User user; + private Text password1; + private Text password2; + private String title; + // private FormToolkit tk; + + public ChangePasswordDialog(User user, String title) { + super(Display.getDefault().getActiveShell()); + // this.tk = tk; + this.user = user; + this.title = title; + } + + protected Control createDialogArea(Composite parent) { + Composite dialogarea = (Composite) super.createDialogArea(parent); + dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + Composite body = new Composite(dialogarea, SWT.NO_FOCUS); + body.setLayoutData(EclipseUiUtils.fillAll()); + GridLayout layout = new GridLayout(2, false); + body.setLayout(layout); + + password1 = createLP(body, "New password", ""); + password2 = createLP(body, "Repeat password", ""); + parent.pack(); + return body; + } + + @SuppressWarnings("unchecked") + @Override + protected void okPressed() { + String msg = null; + + if (password1.getText().equals("")) + msg = "Password cannot be empty"; + else if (password1.getText().equals(password2.getText())) { + char[] newPassword = password1.getText().toCharArray(); + // userAdminWrapper.beginTransactionIfNeeded(); + userAdminWrapper.beginTransactionIfNeeded(); + user.getCredentials().put(null, newPassword); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + super.okPressed(); + } else { + msg = "Passwords are not equals"; + } + + if (EclipseUiUtils.notEmpty(msg)) + MessageDialog.openError(getParentShell(), "Cannot reset pasword", msg); + } + + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText(title); + } + } + + private LdifUsersTable appendMemberOfPart(final Composite parent, User user) { + // Section section = addSection(tk, parent, "Roles"); + // Composite body = (Composite) section.getClient(); + // Composite body= parent; + Composite body = new Composite(parent, SWT.BORDER); + body.setLayout(new GridLayout()); + body.setLayoutData(CmsSwtUtils.fillAll()); + + // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN); + + // Displayed columns + List columnDefs = new ArrayList(); + columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24)); + columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150)); + columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100)); + // Only show technical DN to administrators + // if (isAdmin) + // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", + // 300)); + + // Create and configure the table + final LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL, user); + + userViewerCmp.setColumnDefinitions(columnDefs); + // if (isAdmin) + // userViewerCmp.populateWithStaticFilters(false, false); + // else + userViewerCmp.populate(true, false); + GridData gd = EclipseUiUtils.fillAll(); + gd.heightHint = 500; + userViewerCmp.setLayoutData(gd); + + // Controllers + TableViewer userViewer = userViewerCmp.getTableViewer(); + userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService)); + int operations = DND.DROP_COPY | DND.DROP_MOVE; + Transfer[] tt = new Transfer[] { TextTransfer.getInstance() }; + GroupDropListener dropL = new GroupDropListener(userAdminWrapper, userViewer, user); + userViewer.addDropSupport(operations, tt, dropL); + + AbstractFormPart part = new AbstractFormPart() { + + private GroupChangeListener listener; + + @Override + public void initialize(IManagedForm form) { + super.initialize(form); + listener = new GroupChangeListener(parent.getDisplay(), this); + userAdminWrapper.addListener(listener); + } + + public void commit(boolean onSave) { + super.commit(onSave); + } + + @Override + public void dispose() { + userAdminWrapper.removeListener(listener); + super.dispose(); + } + + @Override + public void refresh() { + userViewerCmp.refresh(); + super.refresh(); + } + }; + getManagedForm().addPart(part); + // addRemoveAbitily(body, userViewer, user); + // userViewerCmp.refresh(); + String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) + " from the below selected groups"; + Action action = new RemoveMembershipAction(userViewer, user, tooltip, SecurityAdminImages.ICON_REMOVE_DESC); + ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + ToolBar toolBar = toolBarManager.createControl(body); + toolBar.setLayoutData(CmsSwtUtils.fillWidth()); + toolBarManager.add(action); + toolBarManager.update(true); + return userViewerCmp; + } + + private class MyUserTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 2653790051461237329L; + + private Button showSystemRoleBtn; + + private final User user; + private final UserFilter userFilter; + + public MyUserTableViewer(Composite parent, int style, User user) { + super(parent, style, true); + this.user = user; + userFilter = new UserFilter(); + } + + protected void populateStaticFilters(Composite staticFilterCmp) { + staticFilterCmp.setLayout(new GridLayout()); + showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK); + showSystemRoleBtn.setText("Show system roles"); + boolean showSysRole = CurrentUser.isInRole(CmsConstants.ROLE_ADMIN); + showSystemRoleBtn.setSelection(showSysRole); + userFilter.setShowSystemRole(showSysRole); + showSystemRoleBtn.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = -7033424592697691676L; + + @Override + public void widgetSelected(SelectionEvent e) { + userFilter.setShowSystemRole(showSystemRoleBtn.getSelection()); + refresh(); + } + }); + } + + @Override + protected List listFilteredElements(String filter) { + List users = (List) getFlatGroups(null); + List filteredUsers = new ArrayList(); + if (users.contains(user)) + users.remove(user); + userFilter.setSearchText(filter); + for (User user : users) + if (userFilter.select(null, null, user)) + filteredUsers.add(user); + return filteredUsers; + } + } + + // private void addRemoveAbility(Composite parent, TableViewer userViewer, User + // user) { + // // Section section = sectionPart.getSection(); + // ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + // ToolBar toolbar = toolBarManager.createControl(parent); + // final Cursor handCursor = new Cursor(Display.getCurrent(), SWT.CURSOR_HAND); + // toolbar.setCursor(handCursor); + // toolbar.addDisposeListener(new DisposeListener() { + // private static final long serialVersionUID = 3882131405820522925L; + // + // public void widgetDisposed(DisposeEvent e) { + // if ((handCursor != null) && (handCursor.isDisposed() == false)) { + // handCursor.dispose(); + // } + // } + // }); + // + // String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) + + // " from the below selected groups"; + // Action action = new RemoveMembershipAction(userViewer, user, tooltip, + // SecurityAdminImages.ICON_REMOVE_DESC); + // toolBarManager.add(action); + // toolBarManager.update(true); + // // section.setTextClient(toolbar); + // } + + private class RemoveMembershipAction extends Action { + private static final long serialVersionUID = -1337713097184522588L; + + private final TableViewer userViewer; + private final User user; + + RemoveMembershipAction(TableViewer userViewer, User user, String name, ImageDescriptor img) { + super(name, img); + this.userViewer = userViewer; + this.user = user; + } + + @Override + public void run() { + ISelection selection = userViewer.getSelection(); + if (selection.isEmpty()) + return; + + @SuppressWarnings("unchecked") + Iterator it = ((IStructuredSelection) selection).iterator(); + List groups = new ArrayList(); + while (it.hasNext()) { + Group currGroup = it.next(); + groups.add(currGroup); + } + + userAdminWrapper.beginTransactionIfNeeded(); + for (Group group : groups) { + group.removeMember(user); + } + userAdminWrapper.commitOrNotifyTransactionStateChange(); + for (Group group : groups) { + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group)); + } + } + } + + /** + * Defines the table as being a potential target to add group memberships + * (roles) to this user + */ + private class GroupDropListener extends ViewerDropAdapter { + private static final long serialVersionUID = 2893468717831451621L; + + private final UserAdminWrapper myUserAdminWrapper; + private final User myUser; + + public GroupDropListener(UserAdminWrapper userAdminWrapper, Viewer userViewer, User user) { + super(userViewer); + this.myUserAdminWrapper = userAdminWrapper; + this.myUser = user; + } + + @Override + public boolean validateDrop(Object target, int operation, TransferData transferType) { + // Target is always OK in a list only view + // TODO check if not a string + boolean validDrop = true; + return validDrop; + } + + @Override + public void drop(DropTargetEvent event) { + String name = (String) event.data; + UserAdmin myUserAdmin = myUserAdminWrapper.getUserAdmin(); + Role role = myUserAdmin.getRole(name); + // TODO this check should be done before. + if (role.getType() == Role.GROUP) { + // TODO check if the user is already member of this group + + myUserAdminWrapper.beginTransactionIfNeeded(); + Group group = (Group) role; + group.addMember(myUser); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + myUserAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group)); + } + super.drop(event); + } + + @Override + public boolean performDrop(Object data) { + // userTableViewerCmp.refresh(); + return true; + } + } + + // LOCAL HELPERS + private void refreshFormTitle(User group) { + // getManagedForm().getForm().setText(UserAdminUtils.getProperty(group, + // LdapAttrs.cn.name())); + } + + /** Appends a section with a title */ + // private Section addSection(FormToolkit tk, Composite parent, String title) { + // Section section = tk.createSection(parent, Section.TITLE_BAR); + // GridData gd = EclipseUiUtils.fillWidth(); + // gd.verticalAlignment = PRE_TITLE_INDENT; + // section.setLayoutData(gd); + // section.setText(title); + // // section.getMenu().setVisible(true); + // + // Composite body = tk.createComposite(section, SWT.WRAP); + // body.setLayoutData(EclipseUiUtils.fillAll()); + // section.setClient(body); + // + // return section; + // } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java new file mode 100644 index 000000000..c6d024ebc --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java @@ -0,0 +1,39 @@ +package org.argeo.cms.e4.users; + +import org.argeo.cms.e4.CmsE4Utils; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.User; + +/** + * Default double click listener for the various user tables, will open the + * clicked item in the editor + */ +public class UserTableDefaultDClickListener implements IDoubleClickListener { + private final EPartService partService; + + public UserTableDefaultDClickListener(EPartService partService) { + this.partService = partService; + } + + public void doubleClick(DoubleClickEvent evt) { + if (evt.getSelection().isEmpty()) + return; + Object obj = ((IStructuredSelection) evt.getSelection()).getFirstElement(); + User user = (User) obj; + + String editorId = getEditorId(user); + CmsE4Utils.openEditor(partService, editorId, LdapAttrs.uid.name(), user.getName()); + } + + protected String getEditorId(User user) { + if (user instanceof Group) + return "org.argeo.cms.e4.partdescriptor.groupEditor"; + else + return "org.argeo.cms.e4.partdescriptor.userEditor"; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UsersView.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UsersView.java new file mode 100644 index 000000000..1d7c873ff --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/UsersView.java @@ -0,0 +1,182 @@ +package org.argeo.cms.e4.users; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.e4.users.providers.CommonNameLP; +import org.argeo.cms.e4.users.providers.DomainNameLP; +import org.argeo.cms.e4.users.providers.MailLP; +import org.argeo.cms.e4.users.providers.UserDragListener; +import org.argeo.cms.e4.users.providers.UserNameLP; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.useradmin.LdifUsersTable; +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdminEvent; +import org.osgi.service.useradmin.UserAdminListener; + +/** List all users with filter - based on Ldif userAdmin */ +public class UsersView { + // private final static Log log = LogFactory.getLog(UsersView.class); + + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".usersView"; + + @Inject + private UserAdminWrapper userAdminWrapper; + @Inject + private EPartService partService; + + // UI Objects + private LdifUsersTable userTableViewerCmp; + private TableViewer userViewer; + private List columnDefs = new ArrayList(); + + private UserAdminListener listener; + + @PostConstruct + public void createPartControl(Composite parent, ESelectionService selectionService) { + + parent.setLayout(EclipseUiUtils.noSpaceGridLayout()); + // Define the displayed columns + columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150)); + columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150)); + columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200)); + // Only show technical DN to admin + if (CurrentUser.isInRole(CmsConstants.ROLE_ADMIN)) + columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300)); + + // Create and configure the table + userTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + userTableViewerCmp.setLayoutData(EclipseUiUtils.fillAll()); + userTableViewerCmp.setColumnDefinitions(columnDefs); + userTableViewerCmp.populate(true, false); + + // Links + userViewer = userTableViewerCmp.getTableViewer(); + userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService)); + userViewer.addSelectionChangedListener(new ISelectionChangedListener() { + + @Override + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + selectionService.setSelection(selection.toList()); + } + }); + // getViewSite().setSelectionProvider(userViewer); + + // Really? + userTableViewerCmp.refresh(); + + // Drag and drop + int operations = DND.DROP_COPY | DND.DROP_MOVE; + Transfer[] tt = new Transfer[] { TextTransfer.getInstance() }; + userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer)); + + // Register a useradmin listener + listener = new MyUiUAListener(parent.getDisplay()); + userAdminWrapper.addListener(listener); + } + + private class MyUiUAListener extends UiUserAdminListener { + public MyUiUAListener(Display display) { + super(display); + } + + @Override + public void roleChangedToUiThread(UserAdminEvent event) { + if (userViewer != null && !userViewer.getTable().isDisposed()) + refresh(); + } + } + + private class MyUserTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 8467999509931900367L; + + private final String[] knownProps = { LdapAttrs.DN, LdapAttrs.uid.name(), LdapAttrs.cn.name(), + LdapAttrs.givenName.name(), LdapAttrs.sn.name(), LdapAttrs.mail.name() }; + + public MyUserTableViewer(Composite parent, int style) { + super(parent, style); + } + + @Override + protected List listFilteredElements(String filter) { + Role[] roles; + + try { + StringBuilder builder = new StringBuilder(); + + StringBuilder tmpBuilder = new StringBuilder(); + if (EclipseUiUtils.notEmpty(filter)) + for (String prop : knownProps) { + tmpBuilder.append("("); + tmpBuilder.append(prop); + tmpBuilder.append("=*"); + tmpBuilder.append(filter); + tmpBuilder.append("*)"); + } + if (tmpBuilder.length() > 1) { + builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.inetOrgPerson.name()).append(")(|"); + builder.append(tmpBuilder.toString()); + builder.append("))"); + } else + builder.append("(").append(LdapAttrs.objectClass.name()).append("=") + .append(LdapObjs.inetOrgPerson.name()).append(")"); + roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString()); + } catch (InvalidSyntaxException e) { + throw new CmsException("Unable to get roles with filter: " + filter, e); + } + List users = new ArrayList(); + for (Role role : roles) + // if (role.getType() == Role.USER && role.getType() != + // Role.GROUP) + users.add((User) role); + return users; + } + } + + public void refresh() { + userTableViewerCmp.refresh(); + } + + // Override generic view methods + @PreDestroy + public void dispose() { + userAdminWrapper.removeListener(listener); + } + + @Focus + public void setFocus() { + userTableViewerCmp.setFocus(); + } + + /* DEPENDENCY INJECTION */ + public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) { + this.userAdminWrapper = userAdminWrapper; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java new file mode 100644 index 000000000..742bc3f5f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java @@ -0,0 +1,95 @@ +package org.argeo.cms.e4.users.handlers; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.GroupsView; +import org.argeo.cms.e4.users.UserAdminWrapper; +import org.eclipse.e4.core.di.annotations.CanExecute; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.UserAdmin; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Delete the selected groups */ +public class DeleteGroups { + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + + // ".deleteGroups"; + + /* DEPENDENCY INJECTION */ + @Inject + private UserAdminWrapper userAdminWrapper; + + @Inject + ESelectionService selectionService; + + @SuppressWarnings("unchecked") + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) { + // ISelection selection = null;// HandlerUtil.getCurrentSelection(event); + // if (selection.isEmpty()) + // return null; + // + // List groups = new ArrayList(); + // Iterator it = ((IStructuredSelection) selection).iterator(); + + List selection = (List) selectionService.getSelection(); + if (selection == null) + return; + + StringBuilder builder = new StringBuilder(); + for (Group group : selection) { + Group currGroup = group; + String groupName = UserAdminUtils.getUserLocalId(currGroup.getName()); + // TODO add checks + builder.append(groupName).append("; "); + // groups.add(currGroup); + } + + if (!MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), "Delete Groups", "Are you sure that you " + + "want to delete these groups?\n" + builder.substring(0, builder.length() - 2))) + return; + + userAdminWrapper.beginTransactionIfNeeded(); + UserAdmin userAdmin = userAdminWrapper.getUserAdmin(); + // IWorkbenchPage iwp = + // HandlerUtil.getActiveWorkbenchWindow(event).getActivePage(); + for (Group group : selection) { + String groupName = group.getName(); + // TODO find a way to close the editor cleanly if opened. Cannot be + // done through the UserAdminListeners, it causes a + // java.util.ConcurrentModificationException because disposing the + // editor unregisters and disposes the listener + // IEditorPart part = iwp.findEditor(new UserEditorInput(groupName)); + // if (part != null) + // iwp.closeEditor(part, false); + userAdmin.removeRole(groupName); + } + userAdminWrapper.commitOrNotifyTransactionStateChange(); + + // Update the view + for (Group group : selection) { + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, group)); + } + + // return null; + } + + @CanExecute + public boolean canExecute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) { + return part.getObject() instanceof GroupsView && selectionService.getSelection() != null; + } + + /* DEPENDENCY INJECTION */ + // public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) { + // this.userAdminWrapper = userAdminWrapper; + // } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java new file mode 100644 index 000000000..d1afd2210 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java @@ -0,0 +1,88 @@ +package org.argeo.cms.e4.users.handlers; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.UserAdminWrapper; +import org.argeo.cms.e4.users.UsersView; +import org.eclipse.e4.core.di.annotations.CanExecute; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.e4.ui.workbench.modeling.ESelectionService; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Delete the selected users */ +public class DeleteUsers { + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".deleteUsers"; + + /* DEPENDENCY INJECTION */ + @Inject + private UserAdminWrapper userAdminWrapper; + + @SuppressWarnings("unchecked") + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) { + // ISelection selection = null;// HandlerUtil.getCurrentSelection(event); + // if (selection.isEmpty()) + // return null; + List selection = (List) selectionService.getSelection(); + if (selection == null) + return; + +// Iterator it = ((IStructuredSelection) selection).iterator(); +// List users = new ArrayList(); + StringBuilder builder = new StringBuilder(); + + for(User user:selection) { + User currUser = user; +// User currUser = it.next(); + String userName = UserAdminUtils.getUserLocalId(currUser.getName()); + if (UserAdminUtils.isCurrentUser(currUser)) { + MessageDialog.openError(Display.getCurrent().getActiveShell(), "Deletion forbidden", + "You cannot delete your own user this way."); + return; + } + builder.append(userName).append("; "); +// users.add(currUser); + } + + if (!MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), "Delete Users", + "Are you sure that you want to delete these users?\n" + builder.substring(0, builder.length() - 2))) + return; + + userAdminWrapper.beginTransactionIfNeeded(); + UserAdmin userAdmin = userAdminWrapper.getUserAdmin(); + // IWorkbenchPage iwp = + // HandlerUtil.getActiveWorkbenchWindow(event).getActivePage(); + + for (User user : selection) { + String userName = user.getName(); + // TODO find a way to close the editor cleanly if opened. Cannot be + // done through the UserAdminListeners, it causes a + // java.util.ConcurrentModificationException because disposing the + // editor unregisters and disposes the listener + // IEditorPart part = iwp.findEditor(new UserEditorInput(userName)); + // if (part != null) + // iwp.closeEditor(part, false); + userAdmin.removeRole(userName); + } + userAdminWrapper.commitOrNotifyTransactionStateChange(); + + for (User user : selection) { + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user)); + } + } + + @CanExecute + public boolean canExecute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) { + return part.getObject() instanceof UsersView && selectionService.getSelection() != null; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java new file mode 100644 index 000000000..41e14e097 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java @@ -0,0 +1,212 @@ +package org.argeo.cms.e4.users.handlers; + +import java.util.Dictionary; +import java.util.Map; + +import javax.inject.Inject; + +import org.argeo.cms.e4.users.UserAdminWrapper; +import org.argeo.cms.swt.CmsException; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Create a new group */ +public class NewGroup { + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newGroup"; + + /* DEPENDENCY INJECTION */ + @Inject + private UserAdminWrapper userAdminWrapper; + + @Execute + public Object execute() { + NewGroupWizard newGroupWizard = new NewGroupWizard(); + newGroupWizard.setWindowTitle("Group creation"); + WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newGroupWizard); + dialog.open(); + return null; + } + + private class NewGroupWizard extends Wizard { + + // Pages + private MainGroupInfoWizardPage mainGroupInfo; + + // UI fields + private Text dNameTxt, commonNameTxt, descriptionTxt; + private Combo baseDnCmb; + + public NewGroupWizard() { + } + + @Override + public void addPages() { + mainGroupInfo = new MainGroupInfoWizardPage(); + addPage(mainGroupInfo); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public boolean performFinish() { + if (!canFinish()) + return false; + String commonName = commonNameTxt.getText(); + try { + userAdminWrapper.beginTransactionIfNeeded(); + String dn = getDn(commonName); + Group group = (Group) userAdminWrapper.getUserAdmin().createRole(dn, Role.GROUP); + Dictionary props = group.getProperties(); + String descStr = descriptionTxt.getText(); + if (EclipseUiUtils.notEmpty(descStr)) + props.put(LdapAttrs.description.name(), descStr); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CREATED, group)); + return true; + } catch (Exception e) { + ErrorFeedback.show("Cannot create new group " + commonName, e); + return false; + } + } + + private class MainGroupInfoWizardPage extends WizardPage implements FocusListener { + private static final long serialVersionUID = -3150193365151601807L; + + public MainGroupInfoWizardPage() { + super("Main"); + setTitle("General information"); + setMessage("Please choose a domain, provide a common name " + "and a free description"); + } + + @Override + public void createControl(Composite parent) { + Composite bodyCmp = new Composite(parent, SWT.NONE); + setControl(bodyCmp); + bodyCmp.setLayout(new GridLayout(2, false)); + + dNameTxt = EclipseUiUtils.createGridLT(bodyCmp, "Distinguished name"); + dNameTxt.setEnabled(false); + + baseDnCmb = createGridLC(bodyCmp, "Base DN"); + // Initialise before adding the listener to avoid NPE + initialiseDnCmb(baseDnCmb); + baseDnCmb.addFocusListener(this); + + commonNameTxt = EclipseUiUtils.createGridLT(bodyCmp, "Common name"); + commonNameTxt.addFocusListener(this); + + Label descLbl = new Label(bodyCmp, SWT.LEAD); + descLbl.setText("Description"); + descLbl.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false)); + descriptionTxt = new Text(bodyCmp, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER); + descriptionTxt.setLayoutData(EclipseUiUtils.fillAll()); + descriptionTxt.addFocusListener(this); + + // Initialize buttons + setPageComplete(false); + getContainer().updateButtons(); + } + + @Override + public void focusLost(FocusEvent event) { + String name = commonNameTxt.getText(); + if (EclipseUiUtils.isEmpty(name)) + dNameTxt.setText(""); + else + dNameTxt.setText(getDn(name)); + + String message = checkComplete(); + if (message != null) { + setMessage(message, WizardPage.ERROR); + setPageComplete(false); + } else { + setMessage("Complete", WizardPage.INFORMATION); + setPageComplete(true); + } + getContainer().updateButtons(); + } + + @Override + public void focusGained(FocusEvent event) { + } + + /** @return the error message or null if complete */ + protected String checkComplete() { + String name = commonNameTxt.getText(); + + if (name.trim().equals("")) + return "Common name must not be empty"; + Role role = userAdminWrapper.getUserAdmin().getRole(getDn(name)); + if (role != null) + return "Group " + name + " already exists"; + return null; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) + if (baseDnCmb.getSelectionIndex() == -1) + baseDnCmb.setFocus(); + else + commonNameTxt.setFocus(); + } + } + + private Map getDns() { + return userAdminWrapper.getKnownBaseDns(true); + } + + private String getDn(String cn) { + Map dns = getDns(); + String bdn = baseDnCmb.getText(); + if (EclipseUiUtils.notEmpty(bdn)) { + Dictionary props = DirectoryConf.uriAsProperties(dns.get(bdn)); + String dn = LdapAttrs.cn.name() + "=" + cn + "," + DirectoryConf.groupBase.getValue(props) + "," + bdn; + return dn; + } + return null; + } + + private void initialiseDnCmb(Combo combo) { + Map dns = userAdminWrapper.getKnownBaseDns(true); + if (dns.isEmpty()) + throw new CmsException("No writable base dn found. Cannot create group"); + combo.setItems(dns.keySet().toArray(new String[0])); + if (dns.size() == 1) + combo.select(0); + } + } + + private Combo createGridLC(Composite parent, String label) { + Label lbl = new Label(parent, SWT.LEAD); + lbl.setText(label); + lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); + Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY); + combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + return combo; + } + + /* DEPENDENCY INJECTION */ + public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) { + this.userAdminWrapper = userAdminWrapper; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java new file mode 100644 index 000000000..40a446006 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java @@ -0,0 +1,287 @@ +package org.argeo.cms.e4.users.handlers; + +import java.util.Dictionary; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.UiAdminUtils; +import org.argeo.cms.e4.users.UserAdminWrapper; +import org.argeo.cms.swt.CmsException; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdminEvent; + +/** Open a wizard that enables creation of a new user. */ +public class NewUser { + // private final static Log log = LogFactory.getLog(NewUser.class); + // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newUser"; + + /* DEPENDENCY INJECTION */ + @Inject + private UserAdminWrapper userAdminWrapper; + + @Execute + public Object execute() { + NewUserWizard newUserWizard = new NewUserWizard(); + newUserWizard.setWindowTitle("User creation"); + WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newUserWizard); + dialog.open(); + return null; + } + + private class NewUserWizard extends Wizard { + + // pages + private MainUserInfoWizardPage mainUserInfo; + + // End user fields + private Text dNameTxt, usernameTxt, firstNameTxt, lastNameTxt, primaryMailTxt, pwd1Txt, pwd2Txt; + private Combo baseDnCmb; + + public NewUserWizard() { + + } + + @Override + public void addPages() { + mainUserInfo = new MainUserInfoWizardPage(); + addPage(mainUserInfo); + String message = "Default wizard that also eases user creation tests:\n " + + "Mail and last name are automatically " + + "generated form the uid. Password are defauted to 'demo'."; + mainUserInfo.setMessage(message, WizardPage.WARNING); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public boolean performFinish() { + if (!canFinish()) + return false; + String username = mainUserInfo.getUsername(); + userAdminWrapper.beginTransactionIfNeeded(); + try { + User user = (User) userAdminWrapper.getUserAdmin().createRole(getDn(username), Role.USER); + + Dictionary props = user.getProperties(); + + String lastNameStr = lastNameTxt.getText(); + if (EclipseUiUtils.notEmpty(lastNameStr)) + props.put(LdapAttrs.sn.name(), lastNameStr); + + String firstNameStr = firstNameTxt.getText(); + if (EclipseUiUtils.notEmpty(firstNameStr)) + props.put(LdapAttrs.givenName.name(), firstNameStr); + + String cn = UserAdminUtils.buildDefaultCn(firstNameStr, lastNameStr); + if (EclipseUiUtils.notEmpty(cn)) + props.put(LdapAttrs.cn.name(), cn); + + String mailStr = primaryMailTxt.getText(); + if (EclipseUiUtils.notEmpty(mailStr)) + props.put(LdapAttrs.mail.name(), mailStr); + + char[] password = mainUserInfo.getPassword(); + user.getCredentials().put(null, password); + userAdminWrapper.commitOrNotifyTransactionStateChange(); + userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CREATED, user)); + return true; + } catch (Exception e) { + ErrorFeedback.show("Cannot create new user " + username, e); + return false; + } + } + + private class MainUserInfoWizardPage extends WizardPage implements ModifyListener { + private static final long serialVersionUID = -3150193365151601807L; + + public MainUserInfoWizardPage() { + super("Main"); + setTitle("Required Information"); + } + + @Override + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + composite.setLayout(new GridLayout(2, false)); + dNameTxt = EclipseUiUtils.createGridLT(composite, "Distinguished name", this); + dNameTxt.setEnabled(false); + + baseDnCmb = createGridLC(composite, "Base DN"); + initialiseDnCmb(baseDnCmb); + baseDnCmb.addModifyListener(this); + baseDnCmb.addModifyListener(new ModifyListener() { + private static final long serialVersionUID = -1435351236582736843L; + + @Override + public void modifyText(ModifyEvent event) { + String name = usernameTxt.getText(); + dNameTxt.setText(getDn(name)); + } + }); + + usernameTxt = EclipseUiUtils.createGridLT(composite, "Local ID", this); + usernameTxt.addModifyListener(new ModifyListener() { + private static final long serialVersionUID = -1435351236582736843L; + + @Override + public void modifyText(ModifyEvent event) { + String name = usernameTxt.getText(); + if (name.trim().equals("")) { + dNameTxt.setText(""); + lastNameTxt.setText(""); + primaryMailTxt.setText(""); + pwd1Txt.setText(""); + pwd2Txt.setText(""); + } else { + dNameTxt.setText(getDn(name)); + lastNameTxt.setText(name.toUpperCase()); + primaryMailTxt.setText(getMail(name)); + pwd1Txt.setText("demo"); + pwd2Txt.setText("demo"); + } + } + }); + + primaryMailTxt = EclipseUiUtils.createGridLT(composite, "Email", this); + firstNameTxt = EclipseUiUtils.createGridLT(composite, "First name", this); + lastNameTxt = EclipseUiUtils.createGridLT(composite, "Last name", this); + pwd1Txt = EclipseUiUtils.createGridLP(composite, "Password", this); + pwd2Txt = EclipseUiUtils.createGridLP(composite, "Repeat password", this); + setControl(composite); + + // Initialize buttons + setPageComplete(false); + getContainer().updateButtons(); + } + + @Override + public void modifyText(ModifyEvent event) { + String message = checkComplete(); + if (message != null) { + setMessage(message, WizardPage.ERROR); + setPageComplete(false); + } else { + setMessage("Complete", WizardPage.INFORMATION); + setPageComplete(true); + } + getContainer().updateButtons(); + } + + /** @return error message or null if complete */ + protected String checkComplete() { + String name = usernameTxt.getText(); + + if (name.trim().equals("")) + return "User name must not be empty"; + Role role = userAdminWrapper.getUserAdmin().getRole(getDn(name)); + if (role != null) + return "User " + name + " already exists"; + if (!primaryMailTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN)) + return "Not a valid email address"; + if (lastNameTxt.getText().trim().equals("")) + return "Specify a last name"; + if (pwd1Txt.getText().trim().equals("")) + return "Specify a password"; + if (pwd2Txt.getText().trim().equals("")) + return "Repeat the password"; + if (!pwd2Txt.getText().equals(pwd1Txt.getText())) + return "Passwords are different"; + return null; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) + if (baseDnCmb.getSelectionIndex() == -1) + baseDnCmb.setFocus(); + else + usernameTxt.setFocus(); + } + + public String getUsername() { + return usernameTxt.getText(); + } + + public char[] getPassword() { + return pwd1Txt.getTextChars(); + } + + } + + private Map getDns() { + return userAdminWrapper.getKnownBaseDns(true); + } + + private String getDn(String uid) { + Map dns = getDns(); + String bdn = baseDnCmb.getText(); + if (EclipseUiUtils.notEmpty(bdn)) { + Dictionary props = DirectoryConf.uriAsProperties(dns.get(bdn)); + String dn = LdapAttrs.uid.name() + "=" + uid + "," + DirectoryConf.userBase.getValue(props) + "," + bdn; + return dn; + } + return null; + } + + private void initialiseDnCmb(Combo combo) { + Map dns = userAdminWrapper.getKnownBaseDns(true); + if (dns.isEmpty()) + throw new CmsException("No writable base dn found. Cannot create user"); + combo.setItems(dns.keySet().toArray(new String[0])); + if (dns.size() == 1) + combo.select(0); + } + + private String getMail(String username) { + if (baseDnCmb.getSelectionIndex() == -1) + return null; + String baseDn = baseDnCmb.getText(); + try { + LdapName name = new LdapName(baseDn); + List rdns = name.getRdns(); + return username + "@" + (String) rdns.get(1).getValue() + '.' + (String) rdns.get(0).getValue(); + } catch (InvalidNameException e) { + throw new CmsException("Unable to generate mail for " + username + " with base dn " + baseDn, e); + } + } + } + + private Combo createGridLC(Composite parent, String label) { + Label lbl = new Label(parent, SWT.LEAD); + lbl.setText(label); + lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); + Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY); + combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + return combo; + } + + /* DEPENDENCY INJECTION */ + public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) { + this.userAdminWrapper = userAdminWrapper; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/package-info.java new file mode 100644 index 000000000..cf3db1d16 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/handlers/package-info.java @@ -0,0 +1,2 @@ +/** Users management handlers. */ +package org.argeo.cms.e4.users.handlers; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/package-info.java new file mode 100644 index 000000000..c6f14b0cf --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/package-info.java @@ -0,0 +1,2 @@ +/** Users management perspective. */ +package org.argeo.cms.e4.users; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java new file mode 100644 index 000000000..2d8db67d7 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java @@ -0,0 +1,21 @@ +package org.argeo.cms.e4.users.providers; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.util.naming.LdapAttrs; +import org.osgi.service.useradmin.User; + +/** Simply declare a label provider that returns the common name of a user */ +public class CommonNameLP extends UserAdminAbstractLP { + private static final long serialVersionUID = 5256703081044911941L; + + @Override + public String getText(User user) { + return UserAdminUtils.getProperty(user, LdapAttrs.cn.name()); + } + + @Override + public String getToolTipText(Object element) { + return UserAdminUtils.getProperty((User) element, LdapAttrs.DN); + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java new file mode 100644 index 000000000..e23729da8 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java @@ -0,0 +1,14 @@ +package org.argeo.cms.e4.users.providers; + +import org.argeo.cms.auth.UserAdminUtils; +import org.osgi.service.useradmin.User; + +/** The human friendly domain name for the corresponding user. */ +public class DomainNameLP extends UserAdminAbstractLP { + private static final long serialVersionUID = 5256703081044911941L; + + @Override + public String getText(User user) { + return UserAdminUtils.getDomainName(user); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/MailLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/MailLP.java new file mode 100644 index 000000000..52d3b858f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/MailLP.java @@ -0,0 +1,15 @@ +package org.argeo.cms.e4.users.providers; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.util.naming.LdapAttrs; +import org.osgi.service.useradmin.User; + +/** Simply declare a label provider that returns the Primary Mail of a user */ +public class MailLP extends UserAdminAbstractLP { + private static final long serialVersionUID = 8329764452141982707L; + + @Override + public String getText(User user) { + return UserAdminUtils.getProperty(user, LdapAttrs.mail.name()); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java new file mode 100644 index 000000000..8e12eeda1 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java @@ -0,0 +1,35 @@ +package org.argeo.cms.e4.users.providers; + +import org.argeo.api.cms.CmsContext; +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.e4.users.SecurityAdminImages; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.swt.graphics.Image; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +/** Provide a bundle specific image depending on the current user type */ +public class RoleIconLP extends UserAdminAbstractLP { + private static final long serialVersionUID = 6550449442061090388L; + + @Override + public String getText(User user) { + return ""; + } + + @Override + public Image getImage(Object element) { + User user = (User) element; + String dn = user.getName(); + if (dn.endsWith(CmsConstants.SYSTEM_ROLES_BASEDN)) + return SecurityAdminImages.ICON_ROLE; + else if (user.getType() == Role.GROUP) { + String businessCategory = UserAdminUtils.getProperty(user, LdapAttrs.businessCategory); + if (businessCategory != null && businessCategory.equals(CmsContext.WORKGROUP)) + return SecurityAdminImages.ICON_WORKGROUP; + return SecurityAdminImages.ICON_GROUP; + } else + return SecurityAdminImages.ICON_USER; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java new file mode 100644 index 000000000..29873db2f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java @@ -0,0 +1,66 @@ +package org.argeo.cms.e4.users.providers; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.cms.swt.CmsException; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.widgets.Display; +import org.osgi.service.useradmin.User; + +/** + * Utility class that add font modifications to a column label provider + * depending on the given user properties + */ +public abstract class UserAdminAbstractLP extends ColumnLabelProvider { + private static final long serialVersionUID = 137336765024922368L; + + // private Font italic; + private Font bold; + + @Override + public Font getFont(Object element) { + // Self as bold + try { + LdapName selfUserName = UserAdminUtils.getCurrentUserLdapName(); + String userName = ((User) element).getName(); + LdapName userLdapName = new LdapName(userName); + if (userLdapName.equals(selfUserName)) { + if (bold == null) + bold = JFaceResources.getFontRegistry() + .defaultFontDescriptor().setStyle(SWT.BOLD) + .createFont(Display.getCurrent()); + return bold; + } + } catch (InvalidNameException e) { + throw new CmsException("cannot parse dn for " + element, e); + } + + // Disabled as Italic + // Node userProfile = (Node) elem; + // if (!userProfile.getProperty(ARGEO_ENABLED).getBoolean()) + // return italic; + + return null; + // return super.getFont(element); + } + + @Override + public String getText(Object element) { + User user = (User) element; + return getText(user); + } + + public void setDisplay(Display display) { + // italic = JFaceResources.getFontRegistry().defaultFontDescriptor() + // .setStyle(SWT.ITALIC).createFont(display); + bold = JFaceResources.getFontRegistry().defaultFontDescriptor() + .setStyle(SWT.BOLD).createFont(Display.getCurrent()); + } + + public abstract String getText(User user); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java new file mode 100644 index 000000000..56a26244b --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java @@ -0,0 +1,40 @@ +package org.argeo.cms.e4.users.providers; + +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.dnd.DragSourceEvent; +import org.eclipse.swt.dnd.DragSourceListener; +import org.osgi.service.useradmin.User; + +/** Default drag listener to modify group and users via the UI */ +public class UserDragListener implements DragSourceListener { + private static final long serialVersionUID = -2074337775033781454L; + private final Viewer viewer; + + public UserDragListener(Viewer viewer) { + this.viewer = viewer; + } + + public void dragStart(DragSourceEvent event) { + // TODO implement finer checks + IStructuredSelection selection = (IStructuredSelection) viewer + .getSelection(); + if (selection.isEmpty() || selection.size() > 1) + event.doit = false; + else + event.doit = true; + } + + public void dragSetData(DragSourceEvent event) { + // TODO Support multiple selection + Object obj = ((IStructuredSelection) viewer.getSelection()) + .getFirstElement(); + if (obj != null) { + User user = (User) obj; + event.data = user.getName(); + } + } + + public void dragFinished(DragSourceEvent event) { + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java new file mode 100644 index 000000000..7a7bfbf56 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java @@ -0,0 +1,58 @@ +package org.argeo.cms.e4.users.providers; + +import static org.argeo.eclipse.ui.EclipseUiUtils.notEmpty; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.UserAdminUtils; +import org.argeo.util.naming.LdapAttrs; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.osgi.service.useradmin.User; + +/** + * Filter user list using JFace mechanism on the client (yet on the server) side + * rather than having the UserAdmin to process the search + */ +public class UserFilter extends ViewerFilter { + private static final long serialVersionUID = 5082509381672880568L; + + private String searchString; + private boolean showSystemRole = true; + + private final String[] knownProps = { LdapAttrs.DN, LdapAttrs.cn.name(), LdapAttrs.givenName.name(), + LdapAttrs.sn.name(), LdapAttrs.uid.name(), LdapAttrs.description.name(), LdapAttrs.mail.name() }; + + public void setSearchText(String s) { + // ensure that the value can be used for matching + if (notEmpty(s)) + searchString = ".*" + s.toLowerCase() + ".*"; + else + searchString = ".*"; + } + + public void setShowSystemRole(boolean showSystemRole) { + this.showSystemRole = showSystemRole; + } + + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + User user = (User) element; + if (!showSystemRole && user.getName().matches(".*(" + CmsConstants.SYSTEM_ROLES_BASEDN + ")")) + // UserAdminUtils.getProperty(user, LdifName.dn.name()) + // .toLowerCase().endsWith(AuthConstants.ROLES_BASEDN)) + return false; + + if (searchString == null || searchString.length() == 0) + return true; + + if (user.getName().matches(searchString)) + return true; + + for (String key : knownProps) { + String currVal = UserAdminUtils.getProperty(user, key); + if (notEmpty(currVal) && currVal.toLowerCase().matches(searchString)) + return true; + } + return false; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java new file mode 100644 index 000000000..3cd00eb2b --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java @@ -0,0 +1,13 @@ +package org.argeo.cms.e4.users.providers; + +import org.osgi.service.useradmin.User; + +/** Simply declare a label provider that returns the username of a user */ +public class UserNameLP extends UserAdminAbstractLP { + private static final long serialVersionUID = 6550449442061090388L; + + @Override + public String getText(User user) { + return user.getName(); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/package-info.java new file mode 100644 index 000000000..33bef8dee --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/e4/users/providers/package-info.java @@ -0,0 +1,2 @@ +/** Users management content providers. */ +package org.argeo.cms.e4.users.providers; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/jcr/e4/rap/CmsE4AdminApp.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/jcr/e4/rap/CmsE4AdminApp.java new file mode 100644 index 000000000..3af44725d --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/jcr/e4/rap/CmsE4AdminApp.java @@ -0,0 +1,17 @@ +package org.argeo.cms.jcr.e4.rap; + +import org.argeo.cms.e4.rap.AbstractRapE4App; +import org.eclipse.rap.rwt.application.Application; + +/** + * Access to canonical views of the core CMS concepts, useful for devleopers and + * operators. + */ +public class CmsE4AdminApp extends AbstractRapE4App { + @Override + protected void addEntryPoints(Application application) { + addE4EntryPoint(application, "/devops", "org.argeo.tool.devops.e4/e4xmi/devops.e4xmi", + customise("Argeo Tool DevOps")); + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/LdifUsersTable.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/LdifUsersTable.java new file mode 100644 index 000000000..a30d2f7ca --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/LdifUsersTable.java @@ -0,0 +1,401 @@ +package org.argeo.cms.swt.useradmin; + +import java.util.ArrayList; +import java.util.List; + +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.eclipse.jface.layout.TableColumnLayout; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; +import org.osgi.service.useradmin.User; + +/** + * Generic composite that display a filter and a table viewer to display users + * (can also be groups) + * + * Warning: this class does not extends TableViewer. Use the + * getTableViewer method to access it. + * + */ +public abstract class LdifUsersTable extends Composite { + private static final long serialVersionUID = -7385959046279360420L; + + // Context + // private UserAdmin userAdmin; + + // Configuration + private List columnDefs = new ArrayList(); + private boolean hasFilter; + private boolean preventTableLayout = false; + private boolean hasSelectionColumn; + private int tableStyle; + + // Local UI Objects + private TableViewer usersViewer; + private Text filterTxt; + + /* EXPOSED METHODS */ + + /** + * @param parent + * @param style + */ + public LdifUsersTable(Composite parent, int style) { + super(parent, SWT.NO_FOCUS); + this.tableStyle = style; + } + + // TODO workaround the bug of the table layout in the Form + public LdifUsersTable(Composite parent, int style, boolean preventTableLayout) { + super(parent, SWT.NO_FOCUS); + this.tableStyle = style; + this.preventTableLayout = preventTableLayout; + } + + /** This must be called before the call to populate method */ + public void setColumnDefinitions(List columnDefinitions) { + this.columnDefs = columnDefinitions; + } + + /** + * + * @param addFilter + * choose to add a field to filter results or not + * @param addSelection + * choose to add a column to select some of the displayed results or + * not + */ + public void populate(boolean addFilter, boolean addSelection) { + // initialization + Composite parent = this; + hasFilter = addFilter; + hasSelectionColumn = addSelection; + + // Main Layout + GridLayout layout = EclipseUiUtils.noSpaceGridLayout(); + layout.verticalSpacing = 5; + this.setLayout(layout); + if (hasFilter) + createFilterPart(parent); + + Composite tableComp = new Composite(parent, SWT.NO_FOCUS); + tableComp.setLayoutData(EclipseUiUtils.fillAll()); + usersViewer = createTableViewer(tableComp); + usersViewer.setContentProvider(new UsersContentProvider()); + } + + /** + * + * @param showMore + * display static filters on creation + * @param addSelection + * choose to add a column to select some of the displayed results or + * not + */ + public void populateWithStaticFilters(boolean showMore, boolean addSelection) { + // initialization + Composite parent = this; + hasFilter = true; + hasSelectionColumn = addSelection; + + // Main Layout + GridLayout layout = EclipseUiUtils.noSpaceGridLayout(); + layout.verticalSpacing = 5; + this.setLayout(layout); + createStaticFilterPart(parent, showMore); + + Composite tableComp = new Composite(parent, SWT.NO_FOCUS); + tableComp.setLayoutData(EclipseUiUtils.fillAll()); + usersViewer = createTableViewer(tableComp); + usersViewer.setContentProvider(new UsersContentProvider()); + } + + /** Enable access to the selected users or groups */ + public List getSelectedUsers() { + if (hasSelectionColumn) { + Object[] elements = ((CheckboxTableViewer) usersViewer).getCheckedElements(); + + List result = new ArrayList(); + for (Object obj : elements) { + result.add((User) obj); + } + return result; + } else + throw new EclipseUiException( + "Unvalid request: no selection column " + "has been created for the current table"); + } + + /** Returns the User table viewer, typically to add doubleclick listener */ + public TableViewer getTableViewer() { + return usersViewer; + } + + /** + * Force the refresh of the underlying table using the current filter string if + * relevant + */ + public void refresh() { + String filter = hasFilter ? filterTxt.getText().trim() : null; + if ("".equals(filter)) + filter = null; + refreshFilteredList(filter); + } + + /** Effective repository request: caller must implement this method */ + abstract protected List listFilteredElements(String filter); + + // protected List listFilteredElements(String filter) { + // List users = new ArrayList(); + // try { + // Role[] roles = userAdmin.getRoles(filter); + // // Display all users and groups + // for (Role role : roles) + // users.add((User) role); + // } catch (InvalidSyntaxException e) { + // throw new EclipseUiException("Unable to get roles with filter: " + // + filter, e); + // } + // return users; + // } + + /* GENERIC COMPOSITE METHODS */ + @Override + public boolean setFocus() { + if (hasFilter) + return filterTxt.setFocus(); + else + return usersViewer.getTable().setFocus(); + } + + @Override + public void dispose() { + super.dispose(); + } + + /* LOCAL CLASSES AND METHODS */ + // Will be usefull to rather use a virtual table viewer + private void refreshFilteredList(String filter) { + List users = listFilteredElements(filter); + usersViewer.setInput(users.toArray()); + } + + private class UsersContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = 1L; + + public Object[] getElements(Object inputElement) { + return (Object[]) inputElement; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } + + /* MANAGE FILTER */ + private void createFilterPart(Composite parent) { + // Text Area for the filter + filterTxt = new Text(parent, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL); + filterTxt.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false)); + filterTxt.addModifyListener(new ModifyListener() { + private static final long serialVersionUID = 1L; + + public void modifyText(ModifyEvent event) { + refreshFilteredList(filterTxt.getText()); + } + }); + } + + private void createStaticFilterPart(Composite parent, boolean showMore) { + Composite filterComp = new Composite(parent, SWT.NO_FOCUS); + filterComp.setLayout(new GridLayout(2, false)); + filterComp.setLayoutData(EclipseUiUtils.fillWidth()); + // generic search + filterTxt = new Text(filterComp, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL); + filterTxt.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false)); + // filterTxt.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL | + // GridData.HORIZONTAL_ALIGN_FILL)); + filterTxt.addModifyListener(new ModifyListener() { + private static final long serialVersionUID = 1L; + + public void modifyText(ModifyEvent event) { + refreshFilteredList(filterTxt.getText()); + } + }); + + // add static filter abilities + Link moreLk = new Link(filterComp, SWT.NONE); + Composite staticFilterCmp = new Composite(filterComp, SWT.NO_FOCUS); + staticFilterCmp.setLayoutData(EclipseUiUtils.fillWidth(2)); + populateStaticFilters(staticFilterCmp); + + MoreLinkListener listener = new MoreLinkListener(moreLk, staticFilterCmp, showMore); + // initialise the layout + listener.refresh(); + moreLk.addSelectionListener(listener); + } + + /** Overwrite to add static filters */ + protected void populateStaticFilters(Composite staticFilterCmp) { + } + + // private void addMoreSL(final Link more) { + // more.addSelectionListener( } + + private class MoreLinkListener extends SelectionAdapter { + private static final long serialVersionUID = -524987616510893463L; + private boolean isShown; + private final Composite staticFilterCmp; + private final Link moreLk; + + public MoreLinkListener(Link moreLk, Composite staticFilterCmp, boolean isShown) { + this.moreLk = moreLk; + this.staticFilterCmp = staticFilterCmp; + this.isShown = isShown; + } + + @Override + public void widgetSelected(SelectionEvent e) { + isShown = !isShown; + refresh(); + } + + public void refresh() { + GridData gd = (GridData) staticFilterCmp.getLayoutData(); + if (isShown) { + moreLk.setText(" Less... "); + gd.heightHint = SWT.DEFAULT; + } else { + moreLk.setText(" More... "); + gd.heightHint = 0; + } + forceLayout(); + } + } + + private void forceLayout() { + LdifUsersTable.this.getParent().layout(true, true); + } + + private TableViewer createTableViewer(final Composite parent) { + + int style = tableStyle | SWT.H_SCROLL | SWT.V_SCROLL; + if (hasSelectionColumn) + style = style | SWT.CHECK; + Table table = new Table(parent, style); + TableColumnLayout layout = new TableColumnLayout(); + + // TODO the table layout does not works with the scrolled form + + if (preventTableLayout) { + parent.setLayout(EclipseUiUtils.noSpaceGridLayout()); + table.setLayoutData(EclipseUiUtils.fillAll()); + } else + parent.setLayout(layout); + + TableViewer viewer; + if (hasSelectionColumn) + viewer = new CheckboxTableViewer(table); + else + viewer = new TableViewer(table); + table.setLinesVisible(true); + table.setHeaderVisible(true); + + TableViewerColumn column; + // int offset = 0; + if (hasSelectionColumn) { + // offset = 1; + column = ViewerUtils.createTableViewerColumn(viewer, "", SWT.NONE, 25); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = 1L; + + @Override + public String getText(Object element) { + return null; + } + }); + layout.setColumnData(column.getColumn(), new ColumnWeightData(25, 25, false)); + + SelectionAdapter selectionAdapter = new SelectionAdapter() { + private static final long serialVersionUID = 1L; + + boolean allSelected = false; + + @Override + public void widgetSelected(SelectionEvent e) { + allSelected = !allSelected; + ((CheckboxTableViewer) usersViewer).setAllChecked(allSelected); + } + }; + column.getColumn().addSelectionListener(selectionAdapter); + } + + // NodeViewerComparator comparator = new NodeViewerComparator(); + // TODO enable the sort by click on the header + // int i = offset; + for (ColumnDefinition colDef : columnDefs) + createTableColumn(viewer, layout, colDef); + + // column = ViewerUtils.createTableViewerColumn(viewer, + // colDef.getHeaderLabel(), SWT.NONE, colDef.getColumnSize()); + // column.setLabelProvider(new CLProvider(colDef.getPropertyName())); + // column.getColumn().addSelectionListener( + // JcrUiUtils.getNodeSelectionAdapter(i, + // colDef.getPropertyType(), colDef.getPropertyName(), + // comparator, viewer)); + // i++; + // } + + // IMPORTANT: initialize comparator before setting it + // JcrColumnDefinition firstCol = colDefs.get(0); + // comparator.setColumn(firstCol.getPropertyType(), + // firstCol.getPropertyName()); + // viewer.setComparator(comparator); + + return viewer; + } + + /** Default creation of a column for a user table */ + private TableViewerColumn createTableColumn(TableViewer tableViewer, TableColumnLayout layout, + ColumnDefinition columnDef) { + + boolean resizable = true; + TableViewerColumn tvc = new TableViewerColumn(tableViewer, SWT.NONE); + TableColumn column = tvc.getColumn(); + + column.setText(columnDef.getLabel()); + column.setWidth(columnDef.getMinWidth()); + column.setResizable(resizable); + + ColumnLabelProvider lp = columnDef.getLabelProvider(); + // add a reference to the display to enable font management + // if (lp instanceof UserAdminAbstractLP) + // ((UserAdminAbstractLP) lp).setDisplay(tableViewer.getTable() + // .getDisplay()); + tvc.setLabelProvider(lp); + + layout.setColumnData(column, new ColumnWeightData(columnDef.getWeight(), columnDef.getMinWidth(), resizable)); + + return tvc; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/PickUpUserDialog.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/PickUpUserDialog.java new file mode 100644 index 000000000..dbf6577d4 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/PickUpUserDialog.java @@ -0,0 +1,245 @@ +package org.argeo.cms.swt.useradmin; + +import java.util.ArrayList; +import java.util.List; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.eclipse.ui.ColumnDefinition; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.TrayDialog; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.FillLayout; +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.Shell; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; + +/** Dialog with a user (or group) list to pick up one */ +public class PickUpUserDialog extends TrayDialog { + private static final long serialVersionUID = -1420106871173920369L; + + // Business objects + private final UserAdmin userAdmin; + private User selectedUser; + + // this page widgets and UI objects + private String title; + private LdifUsersTable userTableViewerCmp; + private TableViewer userViewer; + private List columnDefs = new ArrayList(); + + /** + * A dialog to pick up a group or a user, showing a table with default + * columns + */ + public PickUpUserDialog(Shell parentShell, String title, UserAdmin userAdmin) { + super(parentShell); + this.title = title; + this.userAdmin = userAdmin; + + columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_ICON), "", + 24, 24)); + columnDefs.add(new ColumnDefinition( + new UserLP(UserLP.COL_DISPLAY_NAME), "Common Name", 150, 100)); + columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_DOMAIN), + "Domain", 100, 120)); + columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_DN), + "Distinguished Name", 300, 100)); + } + + /** A dialog to pick up a group or a user */ + public PickUpUserDialog(Shell parentShell, String title, + UserAdmin userAdmin, List columnDefs) { + super(parentShell); + this.title = title; + this.userAdmin = userAdmin; + this.columnDefs = columnDefs; + } + + @Override + protected void okPressed() { + if (getSelected() == null) + MessageDialog.openError(getShell(), "No user chosen", + "Please, choose a user or press Cancel."); + else + super.okPressed(); + } + + protected Control createDialogArea(Composite parent) { + Composite dialogArea = (Composite) super.createDialogArea(parent); + dialogArea.setLayout(new FillLayout()); + + Composite bodyCmp = new Composite(dialogArea, SWT.NO_FOCUS); + bodyCmp.setLayout(new GridLayout()); + + // Create and configure the table + userTableViewerCmp = new MyUserTableViewer(bodyCmp, SWT.MULTI + | SWT.H_SCROLL | SWT.V_SCROLL); + + userTableViewerCmp.setColumnDefinitions(columnDefs); + userTableViewerCmp.populateWithStaticFilters(false, false); + GridData gd = EclipseUiUtils.fillAll(); + gd.minimumHeight = 300; + userTableViewerCmp.setLayoutData(gd); + userTableViewerCmp.refresh(); + + // Controllers + userViewer = userTableViewerCmp.getTableViewer(); + userViewer.addDoubleClickListener(new MyDoubleClickListener()); + userViewer + .addSelectionChangedListener(new MySelectionChangedListener()); + + parent.pack(); + return dialogArea; + } + + public User getSelected() { + if (selectedUser == null) + return null; + else + return selectedUser; + } + + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText(title); + } + + class MyDoubleClickListener implements IDoubleClickListener { + public void doubleClick(DoubleClickEvent evt) { + if (evt.getSelection().isEmpty()) + return; + + Object obj = ((IStructuredSelection) evt.getSelection()) + .getFirstElement(); + if (obj instanceof User) { + selectedUser = (User) obj; + okPressed(); + } + } + } + + class MySelectionChangedListener implements ISelectionChangedListener { + @Override + public void selectionChanged(SelectionChangedEvent event) { + if (event.getSelection().isEmpty()) { + selectedUser = null; + return; + } + Object obj = ((IStructuredSelection) event.getSelection()) + .getFirstElement(); + if (obj instanceof Group) { + selectedUser = (Group) obj; + } + } + } + + private class MyUserTableViewer extends LdifUsersTable { + private static final long serialVersionUID = 8467999509931900367L; + + private final String[] knownProps = { LdapAttrs.uid.name(), + LdapAttrs.cn.name(), LdapAttrs.DN }; + + private Button showSystemRoleBtn; + private Button showUserBtn; + + public MyUserTableViewer(Composite parent, int style) { + super(parent, style); + } + + protected void populateStaticFilters(Composite staticFilterCmp) { + staticFilterCmp.setLayout(new GridLayout()); + showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK); + showSystemRoleBtn.setText("Show system roles "); + + showUserBtn = new Button(staticFilterCmp, SWT.CHECK); + showUserBtn.setText("Show users "); + + SelectionListener sl = new SelectionAdapter() { + private static final long serialVersionUID = -7033424592697691676L; + + @Override + public void widgetSelected(SelectionEvent e) { + refresh(); + } + }; + + showSystemRoleBtn.addSelectionListener(sl); + showUserBtn.addSelectionListener(sl); + } + + @Override + protected List listFilteredElements(String filter) { + Role[] roles; + try { + StringBuilder builder = new StringBuilder(); + + StringBuilder filterBuilder = new StringBuilder(); + if (notNull(filter)) + for (String prop : knownProps) { + filterBuilder.append("("); + filterBuilder.append(prop); + filterBuilder.append("=*"); + filterBuilder.append(filter); + filterBuilder.append("*)"); + } + + String typeStr = "(" + LdapAttrs.objectClass.name() + "=" + + LdapObjs.groupOfNames.name() + ")"; + if ((showUserBtn.getSelection())) + typeStr = "(|(" + LdapAttrs.objectClass.name() + "=" + + LdapObjs.inetOrgPerson.name() + ")" + typeStr + + ")"; + + if (!showSystemRoleBtn.getSelection()) + typeStr = "(& " + typeStr + "(!(" + LdapAttrs.DN + "=*" + + CmsConstants.SYSTEM_ROLES_BASEDN + ")))"; + + if (filterBuilder.length() > 1) { + builder.append("(&" + typeStr); + builder.append("(|"); + builder.append(filterBuilder.toString()); + builder.append("))"); + } else { + builder.append(typeStr); + } + roles = userAdmin.getRoles(builder.toString()); + } catch (InvalidSyntaxException e) { + throw new EclipseUiException( + "Unable to get roles with filter: " + filter, e); + } + List users = new ArrayList(); + for (Role role : roles) + if (!users.contains(role)) + users.add((User) role); + return users; + } + } + + private boolean notNull(String string) { + if (string == null) + return false; + else + return !"".equals(string.trim()); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UserLP.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UserLP.java new file mode 100644 index 000000000..b3ab40ec3 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UserLP.java @@ -0,0 +1,76 @@ +package org.argeo.cms.swt.useradmin; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.UserAdminUtils; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +/** Centralize label providers for the group table */ +class UserLP extends ColumnLabelProvider { + private static final long serialVersionUID = -4645930210988368571L; + + final static String COL_ICON = "colID.icon"; + final static String COL_DN = "colID.dn"; + final static String COL_DISPLAY_NAME = "colID.displayName"; + final static String COL_DOMAIN = "colID.domain"; + + final String currType; + + // private Font italic; + private Font bold; + + UserLP(String colId) { + this.currType = colId; + } + + @Override + public Font getFont(Object element) { + // Current user as bold + if (UserAdminUtils.isCurrentUser(((User) element))) { + if (bold == null) + bold = JFaceResources.getFontRegistry().defaultFontDescriptor().setStyle(SWT.BOLD) + .createFont(Display.getCurrent()); + return bold; + } + return null; + } + + @Override + public Image getImage(Object element) { + if (COL_ICON.equals(currType)) { + User user = (User) element; + String dn = user.getName(); + if (dn.endsWith(CmsConstants.SYSTEM_ROLES_BASEDN)) + return UsersImages.ICON_ROLE; + else if (user.getType() == Role.GROUP) + return UsersImages.ICON_GROUP; + else + return UsersImages.ICON_USER; + } else + return null; + } + + @Override + public String getText(Object element) { + User user = (User) element; + return getText(user); + + } + + public String getText(User user) { + if (COL_DN.equals(currType)) + return user.getName(); + else if (COL_DISPLAY_NAME.equals(currType)) + return UserAdminUtils.getCommonName(user); + else if (COL_DOMAIN.equals(currType)) + return UserAdminUtils.getDomainName(user); + else + return ""; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UsersImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UsersImages.java new file mode 100644 index 000000000..21fc5afba --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/UsersImages.java @@ -0,0 +1,14 @@ +package org.argeo.cms.swt.useradmin; + +import org.argeo.cms.ui.theme.CmsImages; +import org.eclipse.swt.graphics.Image; + +/** Specific users icons. */ +public class UsersImages { + private final static String PREFIX = "icons/"; + + public final static Image ICON_USER = CmsImages.createImg(PREFIX + "person.png"); + public final static Image ICON_GROUP = CmsImages.createImg(PREFIX + "group.png"); + public final static Image ICON_ROLE = CmsImages.createImg(PREFIX + "role.gif"); + public final static Image ICON_CHANGE_PASSWORD = CmsImages.createImg(PREFIX + "security.gif"); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/ViewerUtils.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/ViewerUtils.java new file mode 100644 index 000000000..d186783e9 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/ViewerUtils.java @@ -0,0 +1,58 @@ +package org.argeo.cms.swt.useradmin; + +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TreeColumn; + +/** + * Centralise useful methods to manage JFace Table, Tree and TreeColumn viewers. + */ +public class ViewerUtils { + + /** + * Creates a basic column for the given table. For the time being, we do not + * support movable columns. + */ + public static TableColumn createColumn(Table parent, String name, int style, int width) { + TableColumn result = new TableColumn(parent, style); + result.setText(name); + result.setWidth(width); + result.setResizable(true); + return result; + } + + /** + * Creates a TableViewerColumn for the given viewer. For the time being, we do + * not support movable columns. + */ + public static TableViewerColumn createTableViewerColumn(TableViewer parent, String name, int style, int width) { + TableViewerColumn tvc = new TableViewerColumn(parent, style); + TableColumn column = tvc.getColumn(); + column.setText(name); + column.setWidth(width); + column.setResizable(true); + return tvc; + } + + // public static TableViewerColumn createTableViewerColumn(TableViewer parent, + // Localized name, int style, int width) { + // return createTableViewerColumn(parent, name.lead(), style, width); + // } + + /** + * Creates a TreeViewerColumn for the given viewer. For the time being, we do + * not support movable columns. + */ + public static TreeViewerColumn createTreeViewerColumn(TreeViewer parent, String name, int style, int width) { + TreeViewerColumn tvc = new TreeViewerColumn(parent, style); + TreeColumn column = tvc.getColumn(); + column.setText(name); + column.setWidth(width); + column.setResizable(true); + return tvc; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/package-info.java new file mode 100644 index 000000000..3597bfc57 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/swt/useradmin/package-info.java @@ -0,0 +1,2 @@ +/** SWT/JFace users management components. */ +package org.argeo.cms.swt.useradmin; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java new file mode 100644 index 000000000..4ce468826 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java @@ -0,0 +1,108 @@ +package org.argeo.cms.ui.eclipse.forms; +/** + * AbstractFormPart implements IFormPart interface and can be used as a + * convenient base class for concrete form parts. If a method contains + * code that must be called, look for instructions to call 'super' + * when overriding. + * + * @see org.eclipse.ui.forms.widgets.Section + * @since 1.0 + */ +public abstract class AbstractFormPart implements IFormPart { + private IManagedForm managedForm; + private boolean dirty = false; + private boolean stale = true; + /** + * @see org.eclipse.ui.forms.IFormPart#initialize(org.eclipse.ui.forms.IManagedForm) + */ + public void initialize(IManagedForm form) { + this.managedForm = form; + } + /** + * Returns the form that manages this part. + * + * @return the managed form + */ + public IManagedForm getManagedForm() { + return managedForm; + } + /** + * Disposes the part. Subclasses should override to release any system + * resources. + */ + public void dispose() { + } + /** + * Commits the part. Subclasses should call 'super' when overriding. + * + * @param onSave + * true if the request to commit has arrived as a + * result of the 'save' action. + */ + public void commit(boolean onSave) { + dirty = false; + } + /** + * Sets the overall form input. Subclases may elect to override the method + * and adjust according to the form input. + * + * @param input + * the form input object + * @return false + */ + public boolean setFormInput(Object input) { + return false; + } + /** + * Instructs the part to grab keyboard focus. + */ + public void setFocus() { + } + /** + * Refreshes the section after becoming stale (falling behind data in the + * model). Subclasses must call 'super' when overriding this method. + */ + public void refresh() { + stale = false; + // since we have refreshed, any changes we had in the + // part are gone and we are not dirty + dirty = false; + } + /** + * Marks the part dirty. Subclasses should call this method as a result of + * user interaction with the widgets in the section. + */ + public void markDirty() { + dirty = true; + managedForm.dirtyStateChanged(); + } + /** + * Tests whether the part is dirty i.e. its widgets have state that is + * newer than the data in the model. + * + * @return true if the part is dirty, false + * otherwise. + */ + public boolean isDirty() { + return dirty; + } + /** + * Tests whether the part is stale i.e. its widgets have state that is + * older than the data in the model. + * + * @return true if the part is stale, false + * otherwise. + */ + public boolean isStale() { + return stale; + } + /** + * Marks the part stale. Subclasses should call this method as a result of + * model notification that indicates that the content of the section is no + * longer in sync with the model. + */ + public void markStale() { + stale = true; + managedForm.staleStateChanged(); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormColors.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormColors.java new file mode 100644 index 000000000..32b031b86 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormColors.java @@ -0,0 +1,730 @@ +package org.argeo.cms.ui.eclipse.forms; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGB; +//import org.eclipse.swt.internal.graphics.Graphics; +import org.eclipse.swt.widgets.Display; + +/** + * Manages colors that will be applied to forms and form widgets. The colors are + * chosen to make the widgets look correct in the editor area. If a different + * set of colors is needed, subclass this class and override 'initialize' and/or + * 'initializeColors'. + * + * @since 1.0 + */ +public class FormColors { + /** + * Key for the form title foreground color. + * + * @deprecated use IFormColors.TITLE. + */ + public static final String TITLE = IFormColors.TITLE; + + /** + * Key for the tree/table border color. + * + * @deprecated use IFormColors.BORDER + */ + public static final String BORDER = IFormColors.BORDER; + + /** + * Key for the section separator color. + * + * @deprecated use IFormColors.SEPARATOR. + */ + public static final String SEPARATOR = IFormColors.SEPARATOR; + + /** + * Key for the section title bar background. + * + * @deprecated use IFormColors.TB_BG + */ + public static final String TB_BG = IFormColors.TB_BG; + + /** + * Key for the section title bar foreground. + * + * @deprecated use IFormColors.TB_FG + */ + public static final String TB_FG = IFormColors.TB_FG; + + /** + * Key for the section title bar gradient. + * + * @deprecated use IFormColors.TB_GBG + */ + public static final String TB_GBG = IFormColors.TB_GBG; + + /** + * Key for the section title bar border. + * + * @deprecated use IFormColors.TB_BORDER. + */ + public static final String TB_BORDER = IFormColors.TB_BORDER; + + /** + * Key for the section toggle color. Since 3.1, this color is used for all + * section styles. + * + * @deprecated use IFormColors.TB_TOGGLE. + */ + public static final String TB_TOGGLE = IFormColors.TB_TOGGLE; + + /** + * Key for the section toggle hover color. + * + * @deprecated use IFormColors.TB_TOGGLE_HOVER. + */ + public static final String TB_TOGGLE_HOVER = IFormColors.TB_TOGGLE_HOVER; + + protected Map colorRegistry = new HashMap(10); + + private LocalResourceManager resources; + + protected Color background; + + protected Color foreground; + + private boolean shared; + + protected Display display; + + protected Color border; + + /** + * Creates form colors using the provided display. + * + * @param display + * the display to use + */ + public FormColors(Display display) { + this.display = display; + initialize(); + } + + /** + * Returns the display used to create colors. + * + * @return the display + */ + public Display getDisplay() { + return display; + } + + /** + * Initializes the colors. Subclasses can override this method to change the + * way colors are created. Alternatively, only the color table can be + * modified by overriding initializeColorTable(). + * + * @see #initializeColorTable + */ + protected void initialize() { + background = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND); + foreground = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND); + initializeColorTable(); + updateBorderColor(); + } + + /** + * Allocates colors for the following keys: BORDER, SEPARATOR and + * TITLE. Subclasses can override to allocate these colors differently. + */ + protected void initializeColorTable() { + createTitleColor(); + createColor(IFormColors.SEPARATOR, getColor(IFormColors.TITLE).getRGB()); + RGB black = getSystemColor(SWT.COLOR_BLACK); + RGB borderRGB = getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND_GRADIENT); + createColor(IFormColors.BORDER, blend(borderRGB, black, 80)); + } + + /** + * Allocates colors for the section tool bar (all the keys that start with + * TB). Since these colors are only needed when TITLE_BAR style is used with + * the Section widget, they are not needed all the time and are allocated on + * demand. Consequently, this method will do nothing if the colors have been + * already initialized. Call this method prior to using colors with the TB + * keys to ensure they are available. + */ + public void initializeSectionToolBarColors() { + if (colorRegistry.containsKey(IFormColors.TB_BG)) + return; + createTitleBarGradientColors(); + createTitleBarOutlineColors(); + createTwistieColors(); + } + + /** + * Allocates additional colors for the form header, namely background + * gradients, bottom separator keylines and DND highlights. Since these + * colors are only needed for clients that want to use these particular + * style of header rendering, they are not needed all the time and are + * allocated on demand. Consequently, this method will do nothing if the + * colors have been already initialized. Call this method prior to using + * color keys with the H_ prefix to ensure they are available. + */ + protected void initializeFormHeaderColors() { + if (colorRegistry.containsKey(IFormColors.H_BOTTOM_KEYLINE2)) + return; + createFormHeaderColors(); + } + + /** + * Returns the RGB value of the system color represented by the code + * argument, as defined in SWT class. + * + * @param code + * the system color constant as defined in SWT + * class. + * @return the RGB value of the system color + */ + public RGB getSystemColor(int code) { + return getDisplay().getSystemColor(code).getRGB(); + } + + /** + * Creates the color for the specified key using the provided RGB object. + * The color object will be returned and also put into the registry. When + * the class is disposed, the color will be disposed with it. + * + * @param key + * the unique color key + * @param rgb + * the RGB object + * @return the allocated color object + */ + public Color createColor(String key, RGB rgb) { + // RAP [rh] changes due to missing Color constructor +// Color c = getResourceManager().createColor(rgb); +// Color prevC = (Color) colorRegistry.get(key); +// if (prevC != null && !prevC.isDisposed()) +// getResourceManager().destroyColor(prevC.getRGB()); +// Color c = Graphics.getColor(rgb); + Color c = new Color(display, rgb); + colorRegistry.put(key, c); + return c; + } + + /** + * Creates a color that can be used for areas of the form that is inactive. + * These areas can contain images, links, controls and other content but are + * considered auxilliary to the main content area. + * + *

+ * The color should not be disposed because it is managed by this class. + * + * @return the inactive form color + */ + public Color getInactiveBackground() { + String key = "__ncbg__"; //$NON-NLS-1$ + Color color = getColor(key); + if (color == null) { + RGB sel = getSystemColor(SWT.COLOR_LIST_SELECTION); + // a blend of 95% white and 5% list selection system color + RGB ncbg = blend(sel, getSystemColor(SWT.COLOR_WHITE), 5); + color = createColor(key, ncbg); + } + return color; + } + + /** + * Creates the color for the specified key using the provided RGB values. + * The color object will be returned and also put into the registry. If + * there is already another color object under the same key in the registry, + * the existing object will be disposed. When the class is disposed, the + * color will be disposed with it. + * + * @param key + * the unique color key + * @param r + * red value + * @param g + * green value + * @param b + * blue value + * @return the allocated color object + */ + public Color createColor(String key, int r, int g, int b) { + return createColor(key, new RGB(r,g,b)); + } + + /** + * Computes the border color relative to the background. Allocated border + * color is designed to work well with white. Otherwise, stanard widget + * background color will be used. + */ + protected void updateBorderColor() { + if (isWhiteBackground()) + border = getColor(IFormColors.BORDER); + else { + border = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); + Color bg = getImpliedBackground(); + if (border.getRed() == bg.getRed() + && border.getGreen() == bg.getGreen() + && border.getBlue() == bg.getBlue()) + border = display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW); + } + } + + /** + * Sets the background color. All the toolkits that use this class will + * share the same background. + * + * @param bg + * background color + */ + public void setBackground(Color bg) { + this.background = bg; + updateBorderColor(); + updateFormHeaderColors(); + } + + /** + * Sets the foreground color. All the toolkits that use this class will + * share the same foreground. + * + * @param fg + * foreground color + */ + public void setForeground(Color fg) { + this.foreground = fg; + } + + /** + * Returns the current background color. + * + * @return the background color + */ + public Color getBackground() { + return background; + } + + /** + * Returns the current foreground color. + * + * @return the foreground color + */ + public Color getForeground() { + return foreground; + } + + /** + * Returns the computed border color. Border color depends on the background + * and is recomputed whenever the background changes. + * + * @return the current border color + */ + public Color getBorderColor() { + return border; + } + + /** + * Tests if the background is white. White background has RGB value + * 255,255,255. + * + * @return true if background is white, false + * otherwise. + */ + public boolean isWhiteBackground() { + Color bg = getImpliedBackground(); + return bg.getRed() == 255 && bg.getGreen() == 255 + && bg.getBlue() == 255; + } + + /** + * Returns the color object for the provided key or null if + * not in the registry. + * + * @param key + * the color key + * @return color object if found, or null if not. + */ + public Color getColor(String key) { + if (key.startsWith(IFormColors.TB_PREFIX)) + initializeSectionToolBarColors(); + else if (key.startsWith(IFormColors.H_PREFIX)) + initializeFormHeaderColors(); + return (Color) colorRegistry.get(key); + } + + /** + * Disposes all the colors in the registry. + */ + public void dispose() { + if (resources != null) + resources.dispose(); + resources = null; + colorRegistry = null; + } + + /** + * Marks the colors shared. This prevents toolkits that share this object + * from disposing it. + */ + public void markShared() { + this.shared = true; + } + + /** + * Tests if the colors are shared. + * + * @return true if shared, false otherwise. + */ + public boolean isShared() { + return shared; + } + + /** + * Blends c1 and c2 based in the provided ratio. + * + * @param c1 + * first color + * @param c2 + * second color + * @param ratio + * percentage of the first color in the blend (0-100) + * @return the RGB value of the blended color + */ + public static RGB blend(RGB c1, RGB c2, int ratio) { + int r = blend(c1.red, c2.red, ratio); + int g = blend(c1.green, c2.green, ratio); + int b = blend(c1.blue, c2.blue, ratio); + return new RGB(r, g, b); + } + + /** + * Tests the source RGB for range. + * + * @param rgb + * the tested RGB + * @param from + * range start (excluding the value itself) + * @param to + * range end (excluding the value itself) + * @return true if at least one of the primary colors in the + * source RGB are within the provided range, false + * otherwise. + */ + public static boolean testAnyPrimaryColor(RGB rgb, int from, int to) { + if (testPrimaryColor(rgb.red, from, to)) + return true; + if (testPrimaryColor(rgb.green, from, to)) + return true; + if (testPrimaryColor(rgb.blue, from, to)) + return true; + return false; + } + + /** + * Tests the source RGB for range. + * + * @param rgb + * the tested RGB + * @param from + * range start (excluding the value itself) + * @param to + * tange end (excluding the value itself) + * @return true if at least two of the primary colors in the + * source RGB are within the provided range, false + * otherwise. + */ + public static boolean testTwoPrimaryColors(RGB rgb, int from, int to) { + int total = 0; + if (testPrimaryColor(rgb.red, from, to)) + total++; + if (testPrimaryColor(rgb.green, from, to)) + total++; + if (testPrimaryColor(rgb.blue, from, to)) + total++; + return total >= 2; + } + + /** + * Blends two primary color components based on the provided ratio. + * + * @param v1 + * first component + * @param v2 + * second component + * @param ratio + * percentage of the first component in the blend + * @return + */ + private static int blend(int v1, int v2, int ratio) { + int b = (ratio * v1 + (100 - ratio) * v2) / 100; + return Math.min(255, b); + } + + private Color getImpliedBackground() { + if (getBackground() != null) + return getBackground(); + return getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); + } + + private static boolean testPrimaryColor(int value, int from, int to) { + return value > from && value < to; + } + + private void createTitleColor() { + /* + * RGB rgb = getSystemColor(SWT.COLOR_LIST_SELECTION); // test too light + * if (testTwoPrimaryColors(rgb, 120, 151)) rgb = blend(rgb, BLACK, 80); + * else if (testTwoPrimaryColors(rgb, 150, 256)) rgb = blend(rgb, BLACK, + * 50); createColor(TITLE, rgb); + */ + RGB bg = getImpliedBackground().getRGB(); + RGB listSelection = getSystemColor(SWT.COLOR_LIST_SELECTION); + RGB listForeground = getSystemColor(SWT.COLOR_LIST_FOREGROUND); + RGB rgb = listSelection; + + // Group 1 + // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or + // between 0 and 120, then use 100% LIST_SELECTION as it is (no + // additions) + // Examples: XP Default, Win Classic Standard, Win High Con White, Win + // Classic Marine + if (testTwoPrimaryColors(listSelection, -1, 121)) + rgb = listSelection; + // Group 2 + // When LIST_BACKGROUND = white (255, 255, 255) or not black, text + // colour = LIST_SELECTION @ 100% Opacity + 50% LIST_FOREGROUND over + // LIST_BACKGROUND + // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or + // between 121 and 255, then add 50% LIST_FOREGROUND to LIST_SELECTION + // foreground colour + // Examples: Win Vista, XP Silver, XP Olive , Win Classic Plum, OSX + // Aqua, OSX Graphite, Linux GTK + else if (testTwoPrimaryColors(listSelection, 120, 256) + || (bg.red == 0 && bg.green == 0 && bg.blue == 0)) + rgb = blend(listSelection, listForeground, 50); + // Group 3 + // When LIST_BACKGROUND = black (0, 0, 0), text colour = LIST_SELECTION + // @ 100% Opacity + 50% LIST_FOREGROUND over LIST_BACKGROUND + // Rule: If LIST_BACKGROUND = 0, 0, 0, then add 50% LIST_FOREGROUND to + // LIST_SELECTION foreground colour + // Examples: Win High Con Black, Win High Con #1, Win High Con #2 + // (covered in the second part of the OR clause above) + createColor(IFormColors.TITLE, rgb); + } + + private void createTwistieColors() { + RGB rgb = getColor(IFormColors.TITLE).getRGB(); + RGB white = getSystemColor(SWT.COLOR_WHITE); + createColor(TB_TOGGLE, rgb); + rgb = blend(rgb, white, 60); + createColor(TB_TOGGLE_HOVER, rgb); + } + + private void createTitleBarGradientColors() { + RGB tbBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND); + RGB bg = getImpliedBackground().getRGB(); + + // Group 1 + // Rule: If at least 2 of the RGB values are equal to or between 180 and + // 255, then apply specified opacity for Group 1 + // Examples: Vista, XP Silver, Wn High Con #2 + // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND + // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND + if (testTwoPrimaryColors(tbBg, 179, 256)) + tbBg = blend(tbBg, bg, 30); + + // Group 2 + // Rule: If at least 2 of the RGB values are equal to or between 121 and + // 179, then apply specified opacity for Group 2 + // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black + // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND + // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND + else if (testTwoPrimaryColors(tbBg, 120, 180)) + tbBg = blend(tbBg, bg, 20); + + // Group 3 + // Rule: Everything else + // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX + // Aqua, Wn High Con White, Wn High Con #1 + // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND + // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND + else { + tbBg = blend(tbBg, bg, 10); + } + + createColor(IFormColors.TB_BG, tbBg); + + // for backward compatibility + createColor(TB_GBG, tbBg); + } + + private void createTitleBarOutlineColors() { + // title bar outline - border color + RGB tbBorder = getSystemColor(SWT.COLOR_TITLE_BACKGROUND); + RGB bg = getImpliedBackground().getRGB(); + // Group 1 + // Rule: If at least 2 of the RGB values are equal to or between 180 and + // 255, then apply specified opacity for Group 1 + // Examples: Vista, XP Silver, Wn High Con #2 + // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND + if (testTwoPrimaryColors(tbBorder, 179, 256)) + tbBorder = blend(tbBorder, bg, 70); + + // Group 2 + // Rule: If at least 2 of the RGB values are equal to or between 121 and + // 179, then apply specified opacity for Group 2 + // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black + + // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND + else if (testTwoPrimaryColors(tbBorder, 120, 180)) + tbBorder = blend(tbBorder, bg, 50); + + // Group 3 + // Rule: Everything else + // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX + // Aqua, Wn High Con White, Wn High Con #1 + + // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND + else { + tbBorder = blend(tbBorder, bg, 30); + } + createColor(FormColors.TB_BORDER, tbBorder); + } + + private void updateFormHeaderColors() { + if (colorRegistry.containsKey(IFormColors.H_GRADIENT_END)) { + disposeIfFound(IFormColors.H_GRADIENT_END); + disposeIfFound(IFormColors.H_GRADIENT_START); + disposeIfFound(IFormColors.H_BOTTOM_KEYLINE1); + disposeIfFound(IFormColors.H_BOTTOM_KEYLINE2); + disposeIfFound(IFormColors.H_HOVER_LIGHT); + disposeIfFound(IFormColors.H_HOVER_FULL); + initializeFormHeaderColors(); + } + } + + private void disposeIfFound(String key) { + Color color = getColor(key); + if (color != null) { + colorRegistry.remove(key); + // RAP [rh] changes due to missing Color#dispose() +// color.dispose(); + } + } + + private void createFormHeaderColors() { + createFormHeaderGradientColors(); + createFormHeaderKeylineColors(); + createFormHeaderDNDColors(); + } + + private void createFormHeaderGradientColors() { + RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND); + Color bgColor = getImpliedBackground(); + RGB bg = bgColor.getRGB(); + RGB bottom, top; + // Group 1 + // Rule: If at least 2 of the RGB values are equal to or between 180 and + // 255, then apply specified opacity for Group 1 + // Examples: Vista, XP Silver, Wn High Con #2 + // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND + // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND + if (testTwoPrimaryColors(titleBg, 179, 256)) { + bottom = blend(titleBg, bg, 30); + top = bg; + } + + // Group 2 + // Rule: If at least 2 of the RGB values are equal to or between 121 and + // 179, then apply specified opacity for Group 2 + // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black + // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND + // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND + else if (testTwoPrimaryColors(titleBg, 120, 180)) { + bottom = blend(titleBg, bg, 20); + top = bg; + } + + // Group 3 + // Rule: If at least 2 of the RGB values are equal to or between 0 and + // 120, then apply specified opacity for Group 3 + // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX + // Aqua, Wn High Con White, Wn High Con #1 + // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND + // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND + else { + bottom = blend(titleBg, bg, 10); + top = bg; + } + createColor(IFormColors.H_GRADIENT_END, top); + createColor(IFormColors.H_GRADIENT_START, bottom); + } + + private void createFormHeaderKeylineColors() { + RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND); + Color bgColor = getImpliedBackground(); + RGB bg = bgColor.getRGB(); + RGB keyline2; + // H_BOTTOM_KEYLINE1 + createColor(IFormColors.H_BOTTOM_KEYLINE1, new RGB(255, 255, 255)); + + // H_BOTTOM_KEYLINE2 + // Group 1 + // Rule: If at least 2 of the RGB values are equal to or between 180 and + // 255, then apply specified opacity for Group 1 + // Examples: Vista, XP Silver, Wn High Con #2 + // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND + if (testTwoPrimaryColors(titleBg, 179, 256)) + keyline2 = blend(titleBg, bg, 70); + + // Group 2 + // Rule: If at least 2 of the RGB values are equal to or between 121 and + // 179, then apply specified opacity for Group 2 + // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black + // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND + else if (testTwoPrimaryColors(titleBg, 120, 180)) + keyline2 = blend(titleBg, bg, 50); + + // Group 3 + // Rule: If at least 2 of the RGB values are equal to or between 0 and + // 120, then apply specified opacity for Group 3 + // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX + // Aqua, Wn High Con White, Wn High Con #1 + + // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND + else + keyline2 = blend(titleBg, bg, 30); + // H_BOTTOM_KEYLINE2 + createColor(IFormColors.H_BOTTOM_KEYLINE2, keyline2); + } + + private void createFormHeaderDNDColors() { + RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT); + Color bgColor = getImpliedBackground(); + RGB bg = bgColor.getRGB(); + RGB light, full; + // ALL Themes + // + // Light Highlight + // When *near* the 'hot' area + // Rule: If near the title in the 'hot' area, show background highlight + // TITLE_BACKGROUND_GRADIENT @ 40% + light = blend(titleBg, bg, 40); + // Full Highlight + // When *on* the title area (regions 1 and 2) + // Rule: If near the title in the 'hot' area, show background highlight + // TITLE_BACKGROUND_GRADIENT @ 60% + full = blend(titleBg, bg, 60); + // H_DND_LIGHT + // H_DND_FULL + createColor(IFormColors.H_HOVER_LIGHT, light); + createColor(IFormColors.H_HOVER_FULL, full); + } + + private LocalResourceManager getResourceManager() { + if (resources == null) + resources = new LocalResourceManager(JFaceResources.getResources()); + return resources; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java new file mode 100644 index 000000000..9e931ba50 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java @@ -0,0 +1,122 @@ +package org.argeo.cms.ui.eclipse.forms; + +import java.util.HashMap; + +import org.eclipse.jface.resource.DeviceResourceException; +import org.eclipse.jface.resource.FontDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +//import org.eclipse.swt.internal.graphics.Graphics; +import org.eclipse.swt.widgets.Display; + +public class FormFonts { + private static FormFonts instance; + + public static FormFonts getInstance() { + if (instance == null) + instance = new FormFonts(); + return instance; + } + + private LocalResourceManager resources; + private HashMap descriptors; + + private FormFonts() { + } + + private class BoldFontDescriptor extends FontDescriptor { + private FontData[] fFontData; + + BoldFontDescriptor(Font font) { + // RAP [if] Changes due to different way of creating fonts + // fFontData = font.getFontData(); + // for (int i = 0; i < fFontData.length; i++) { + // fFontData[i].setStyle(fFontData[i].getStyle() | SWT.BOLD); + // } + FontData fontData = font.getFontData()[0]; + // Font boldFont = Graphics.getFont( fontData.getName(), + // fontData.getHeight(), + // fontData.getStyle() | SWT.BOLD ); + Font boldFont = new Font(Display.getCurrent(), fontData.getName(), fontData.getHeight(), + fontData.getStyle() | SWT.BOLD); + fFontData = boldFont.getFontData(); + } + + public boolean equals(Object obj) { + if (obj instanceof BoldFontDescriptor) { + BoldFontDescriptor desc = (BoldFontDescriptor) obj; + if (desc.fFontData.length != fFontData.length) + return false; + for (int i = 0; i < fFontData.length; i++) + if (!fFontData[i].equals(desc.fFontData[i])) + return false; + return true; + } + return false; + } + + public int hashCode() { + int hash = 0; + for (int i = 0; i < fFontData.length; i++) + hash = hash * 7 + fFontData[i].hashCode(); + return hash; + } + + public Font createFont(Device device) throws DeviceResourceException { + // RAP [if] Changes due to different way of creating fonts + return new Font(device, fFontData[0]); + // return Graphics.getFont( fFontData[ 0 ] ); + } + + public void destroyFont(Font previouslyCreatedFont) { + // RAP [if] unnecessary + // previouslyCreatedFont.dispose(); + } + } + + public Font getBoldFont(Display display, Font font) { + checkHashMaps(); + BoldFontDescriptor desc = new BoldFontDescriptor(font); + Font result = getResourceManager().createFont(desc); + descriptors.put(result, desc); + return result; + } + + public boolean markFinished(Font boldFont) { + checkHashMaps(); + BoldFontDescriptor desc = (BoldFontDescriptor) descriptors.get(boldFont); + if (desc != null) { + getResourceManager().destroyFont(desc); + if (getResourceManager().find(desc) == null) { + descriptors.remove(boldFont); + validateHashMaps(); + } + return true; + + } + // if the image was not found, dispose of it for the caller + // RAP [if] unnecessary + // boldFont.dispose(); + return false; + } + + private LocalResourceManager getResourceManager() { + if (resources == null) + resources = new LocalResourceManager(JFaceResources.getResources()); + return resources; + } + + private void checkHashMaps() { + if (descriptors == null) + descriptors = new HashMap(); + } + + private void validateHashMaps() { + if (descriptors.size() == 0) + descriptors = null; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java new file mode 100644 index 000000000..99271046a --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java @@ -0,0 +1,913 @@ +package org.argeo.cms.ui.eclipse.forms; + +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +//import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +// RAP [rh] Paint events missing +//import org.eclipse.swt.events.PaintEvent; +//import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +//RAP [rh] GC missing +//import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +//import org.eclipse.swt.graphics.RGB; +//import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +//import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +//import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.Widget; +//import org.eclipse.ui.forms.FormColors; +//import org.eclipse.ui.forms.HyperlinkGroup; +//import org.eclipse.ui.forms.IFormColors; +//import org.eclipse.ui.internal.forms.widgets.FormFonts; +//import org.eclipse.ui.internal.forms.widgets.FormUtil; + +/** + * The toolkit is responsible for creating SWT controls adapted to work in + * Eclipse forms. In addition to changing their presentation properties (fonts, + * colors etc.), various listeners are attached to make them behave correctly in + * the form context. + *

+ * In addition to being the control factory, the toolkit is also responsible for + * painting flat borders for select controls, managing hyperlink groups and + * control colors. + *

+ * The toolkit creates some of the most common controls used to populate Eclipse + * forms. Controls that must be created using their constructors, + * adapt() method is available to change its properties in the + * same way as with the supported toolkit controls. + *

+ * Typically, one toolkit object is created per workbench part (for example, an + * editor or a form wizard). The toolkit is disposed when the part is disposed. + * To conserve resources, it is possible to create one color object for the + * entire plug-in and share it between several toolkits. The plug-in is + * responsible for disposing the colors (disposing the toolkit that uses shared + * color object will not dispose the colors). + *

+ * FormToolkit is normally instantiated, but can also be subclassed if some of + * the methods needs to be modified. In those cases, super must + * be called to preserve normal behaviour. + * + * @since 1.0 + */ +public class FormToolkit { + public static final String KEY_DRAW_BORDER = "FormWidgetFactory.drawBorder"; //$NON-NLS-1$ + + public static final String TREE_BORDER = "treeBorder"; //$NON-NLS-1$ + + public static final String TEXT_BORDER = "textBorder"; //$NON-NLS-1$ + + private int borderStyle = SWT.NULL; + + private FormColors colors; + + private int orientation = Window.getDefaultOrientation(); + + // private KeyListener deleteListener; + // RAP [rh] Paint events missing +// private BorderPainter borderPainter; + + private BoldFontHolder boldFontHolder; + +// private HyperlinkGroup hyperlinkGroup; + + private boolean isDisposed = false; + + /* default */ + VisibilityHandler visibilityHandler; + + /* default */ + KeyboardHandler keyboardHandler; + + // RAP [rh] Paint events missing +// private class BorderPainter implements PaintListener { +// public void paintControl(PaintEvent event) { +// Composite composite = (Composite) event.widget; +// Control[] children = composite.getChildren(); +// for (int i = 0; i < children.length; i++) { +// Control c = children[i]; +// boolean inactiveBorder = false; +// boolean textBorder = false; +// if (!c.isVisible()) +// continue; +// /* +// * if (c.getEnabled() == false && !(c instanceof CCombo)) +// * continue; +// */ +// if (c instanceof Hyperlink) +// continue; +// Object flag = c.getData(KEY_DRAW_BORDER); +// if (flag != null) { +// if (flag.equals(Boolean.FALSE)) +// continue; +// if (flag.equals(TREE_BORDER)) +// inactiveBorder = true; +// else if (flag.equals(TEXT_BORDER)) +// textBorder = true; +// } +// if (getBorderStyle() == SWT.BORDER) { +// if (!inactiveBorder && !textBorder) { +// continue; +// } +// if (c instanceof Text || c instanceof Table +// || c instanceof Tree) +// continue; +// } +// if (!inactiveBorder +// && (c instanceof Text || c instanceof CCombo || textBorder)) { +// Rectangle b = c.getBounds(); +// GC gc = event.gc; +// gc.setForeground(c.getBackground()); +// gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1, +// b.height + 1); +// // gc.setForeground(getBorderStyle() == SWT.BORDER ? colors +// // .getBorderColor() : colors.getForeground()); +// gc.setForeground(colors.getBorderColor()); +// if (c instanceof CCombo) +// gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1, +// b.height + 1); +// else +// gc.drawRectangle(b.x - 1, b.y - 2, b.width + 1, +// b.height + 3); +// } else if (inactiveBorder || c instanceof Table +// || c instanceof Tree) { +// Rectangle b = c.getBounds(); +// GC gc = event.gc; +// gc.setForeground(colors.getBorderColor()); +// gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1, +// b.height + 1); +// } +// } +// } +// } + + private static class VisibilityHandler extends FocusAdapter { + public void focusGained(FocusEvent e) { + Widget w = e.widget; + if (w instanceof Control) { + FormUtil.ensureVisible((Control) w); + } + } + } + + private static class KeyboardHandler extends KeyAdapter { + public void keyPressed(KeyEvent e) { + Widget w = e.widget; + if (w instanceof Control) { + if (e.doit) + FormUtil.processKey(e.keyCode, (Control) w); + } + } + } + + private class BoldFontHolder { + private Font normalFont; + + private Font boldFont; + + public BoldFontHolder() { + } + + public Font getBoldFont(Font font) { + createBoldFont(font); + return boldFont; + } + + private void createBoldFont(Font font) { + if (normalFont == null || !normalFont.equals(font)) { + normalFont = font; + dispose(); + } + if (boldFont == null) { + boldFont = FormFonts.getInstance().getBoldFont(colors.getDisplay(), + normalFont); + } + } + + public void dispose() { + if (boldFont != null) { + FormFonts.getInstance().markFinished(boldFont); + boldFont = null; + } + } + } + + /** + * Creates a toolkit that is self-sufficient (will manage its own colors). + *

+ * Clients that call this method must call {@link #dispose()} when they + * are finished using the toolkit. + * + */ + public FormToolkit(Display display) { + this(new FormColors(display)); + } + + /** + * Creates a toolkit that will use the provided (shared) colors. The toolkit + * will dispose the colors if and only if they are not marked as + * shared via the markShared() method. + *

+ * Clients that call this method must call {@link #dispose()} when they + * are finished using the toolkit. + * + * @param colors + * the shared colors + */ + public FormToolkit(FormColors colors) { + this.colors = colors; + initialize(); + } + + /** + * Creates a button as a part of the form. + * + * @param parent + * the button parent + * @param text + * an optional text for the button (can be null) + * @param style + * the button style (for example, SWT.PUSH) + * @return the button widget + */ + public Button createButton(Composite parent, String text, int style) { + Button button = new Button(parent, style | SWT.FLAT | orientation); + if (text != null) + button.setText(text); + adapt(button, true, true); + return button; + } + + /** + * Creates the composite as a part of the form. + * + * @param parent + * the composite parent + * @return the composite widget + */ + public Composite createComposite(Composite parent) { + return createComposite(parent, SWT.NULL); + } + + /** + * Creates the composite as part of the form using the provided style. + * + * @param parent + * the composite parent + * @param style + * the composite style + * @return the composite widget + */ + public Composite createComposite(Composite parent, int style) { +// Composite composite = new LayoutComposite(parent, style | orientation); + Composite composite = new Composite(parent, style | orientation); + adapt(composite); + return composite; + } + + /** + * Creats the composite that can server as a separator between various parts + * of a form. Separator height should be controlled by setting the height + * hint on the layout data for the composite. + * + * @param parent + * the separator parent + * @return the separator widget + */ +// RAP [rh] createCompositeSeparator: currently no useful implementation possible, delete? + public Composite createCompositeSeparator(Composite parent) { + final Composite composite = new Composite(parent, orientation); +// RAP [rh] GC and paint events missing +// composite.addListener(SWT.Paint, new Listener() { +// public void handleEvent(Event e) { +// if (composite.isDisposed()) +// return; +// Rectangle bounds = composite.getBounds(); +// GC gc = e.gc; +// gc.setForeground(colors.getColor(IFormColors.SEPARATOR)); +// if (colors.getBackground() != null) +// gc.setBackground(colors.getBackground()); +// gc.fillGradientRectangle(0, 0, bounds.width, bounds.height, +// false); +// } +// }); +// if (parent instanceof Section) +// ((Section) parent).setSeparatorControl(composite); + return composite; + } + + /** + * Creates a label as a part of the form. + * + * @param parent + * the label parent + * @param text + * the label text + * @return the label widget + */ + public Label createLabel(Composite parent, String text) { + return createLabel(parent, text, SWT.NONE); + } + + /** + * Creates a label as a part of the form. + * + * @param parent + * the label parent + * @param text + * the label text + * @param style + * the label style + * @return the label widget + */ + public Label createLabel(Composite parent, String text, int style) { + Label label = new Label(parent, style | orientation); + if (text != null) + label.setText(text); + adapt(label, false, false); + return label; + } + + /** + * Creates a hyperlink as a part of the form. The hyperlink will be added to + * the hyperlink group that belongs to this toolkit. + * + * @param parent + * the hyperlink parent + * @param text + * the text of the hyperlink + * @param style + * the hyperlink style + * @return the hyperlink widget + */ +// public Hyperlink createHyperlink(Composite parent, String text, int style) { +// Hyperlink hyperlink = new Hyperlink(parent, style | orientation); +// if (text != null) +// hyperlink.setText(text); +// hyperlink.addFocusListener(visibilityHandler); +// hyperlink.addKeyListener(keyboardHandler); +// hyperlinkGroup.add(hyperlink); +// return hyperlink; +// } + + /** + * Creates an image hyperlink as a part of the form. The hyperlink will be + * added to the hyperlink group that belongs to this toolkit. + * + * @param parent + * the hyperlink parent + * @param style + * the hyperlink style + * @return the image hyperlink widget + */ +// public ImageHyperlink createImageHyperlink(Composite parent, int style) { +// ImageHyperlink hyperlink = new ImageHyperlink(parent, style +// | orientation); +// hyperlink.addFocusListener(visibilityHandler); +// hyperlink.addKeyListener(keyboardHandler); +// hyperlinkGroup.add(hyperlink); +// return hyperlink; +// } + + /** + * Creates a rich text as a part of the form. + * + * @param parent + * the rich text parent + * @param trackFocus + * if true, the toolkit will monitor focus + * transfers to ensure that the hyperlink in focus is visible in + * the form. + * @return the rich text widget + * @since 1.2 + */ +// public FormText createFormText(Composite parent, boolean trackFocus) { +// FormText engine = new FormText(parent, SWT.WRAP | orientation); +// engine.marginWidth = 1; +// engine.marginHeight = 0; +// engine.setHyperlinkSettings(getHyperlinkGroup()); +// adapt(engine, trackFocus, true); +// engine.setMenu(parent.getMenu()); +// return engine; +// } + + /** + * Adapts a control to be used in a form that is associated with this + * toolkit. This involves adjusting colors and optionally adding handlers to + * ensure focus tracking and keyboard management. + * + * @param control + * a control to adapt + * @param trackFocus + * if true, form will be scrolled horizontally + * and/or vertically if needed to ensure that the control is + * visible when it gains focus. Set it to false if + * the control is not capable of gaining focus. + * @param trackKeyboard + * if true, the control that is capable of + * gaining focus will be tracked for certain keys that are + * important to the underlying form (for example, PageUp, + * PageDown, ScrollUp, ScrollDown etc.). Set it to + * false if the control is not capable of gaining + * focus or these particular key event are already used by the + * control. + */ + public void adapt(Control control, boolean trackFocus, boolean trackKeyboard) { + control.setBackground(colors.getBackground()); + control.setForeground(colors.getForeground()); +// if (control instanceof ExpandableComposite) { +// ExpandableComposite ec = (ExpandableComposite) control; +// if (ec.toggle != null) { +// if (trackFocus) +// ec.toggle.addFocusListener(visibilityHandler); +// if (trackKeyboard) +// ec.toggle.addKeyListener(keyboardHandler); +// } +// if (ec.textLabel != null) { +// if (trackFocus) +// ec.textLabel.addFocusListener(visibilityHandler); +// if (trackKeyboard) +// ec.textLabel.addKeyListener(keyboardHandler); +// } +// return; +// } + if (trackFocus) + control.addFocusListener(visibilityHandler); + if (trackKeyboard) + control.addKeyListener(keyboardHandler); + } + + /** + * Adapts a composite to be used in a form associated with this toolkit. + * + * @param composite + * the composite to adapt + */ + public void adapt(Composite composite) { + composite.setBackground(colors.getBackground()); + composite.addMouseListener(new MouseAdapter() { + public void mouseDown(MouseEvent e) { + ((Control) e.widget).setFocus(); + } + }); + if (composite.getParent() != null) + composite.setMenu(composite.getParent().getMenu()); + } + + /** + * A helper method that ensures the provided control is visible when + * ScrolledComposite is somewhere in the parent chain. If scroll bars are + * visible and the control is clipped, the client of the scrolled composite + * will be scrolled to reveal the control. + * + * @param c + * the control to reveal + */ + public static void ensureVisible(Control c) { + FormUtil.ensureVisible(c); + } + + /** + * Creates a section as a part of the form. + * + * @param parent + * the section parent + * @param sectionStyle + * the section style + * @return the section widget + */ +// public Section createSection(Composite parent, int sectionStyle) { +// Section section = new Section(parent, orientation, sectionStyle); +// section.setMenu(parent.getMenu()); +// adapt(section, true, true); +// if (section.toggle != null) { +// section.toggle.setHoverDecorationColor(colors +// .getColor(IFormColors.TB_TOGGLE_HOVER)); +// section.toggle.setDecorationColor(colors +// .getColor(IFormColors.TB_TOGGLE)); +// } +// section.setFont(boldFontHolder.getBoldFont(parent.getFont())); +// if ((sectionStyle & Section.TITLE_BAR) != 0 +// || (sectionStyle & Section.SHORT_TITLE_BAR) != 0) { +// colors.initializeSectionToolBarColors(); +// section.setTitleBarBackground(colors.getColor(IFormColors.TB_BG)); +// section.setTitleBarBorderColor(colors +// .getColor(IFormColors.TB_BORDER)); +// } +// // call setTitleBarForeground regardless as it also sets the label color +// section.setTitleBarForeground(colors +// .getColor(IFormColors.TB_TOGGLE)); +// return section; +// } + + /** + * Creates an expandable composite as a part of the form. + * + * @param parent + * the expandable composite parent + * @param expansionStyle + * the expandable composite style + * @return the expandable composite widget + */ +// public ExpandableComposite createExpandableComposite(Composite parent, +// int expansionStyle) { +// ExpandableComposite ec = new ExpandableComposite(parent, orientation, +// expansionStyle); +// ec.setMenu(parent.getMenu()); +// adapt(ec, true, true); +// ec.setFont(boldFontHolder.getBoldFont(ec.getFont())); +// return ec; +// } + + /** + * Creates a separator label as a part of the form. + * + * @param parent + * the separator parent + * @param style + * the separator style + * @return the separator label + */ + public Label createSeparator(Composite parent, int style) { + Label label = new Label(parent, SWT.SEPARATOR | style | orientation); + label.setBackground(colors.getBackground()); + label.setForeground(colors.getBorderColor()); + return label; + } + + /** + * Creates a table as a part of the form. + * + * @param parent + * the table parent + * @param style + * the table style + * @return the table widget + */ + public Table createTable(Composite parent, int style) { + Table table = new Table(parent, style | borderStyle | orientation); + adapt(table, false, false); + // hookDeleteListener(table); + return table; + } + + /** + * Creates a text as a part of the form. + * + * @param parent + * the text parent + * @param value + * the text initial value + * @return the text widget + */ + public Text createText(Composite parent, String value) { + return createText(parent, value, SWT.SINGLE); + } + + /** + * Creates a text as a part of the form. + * + * @param parent + * the text parent + * @param value + * the text initial value + * @param style + * the text style + * @return the text widget + */ + public Text createText(Composite parent, String value, int style) { + Text text = new Text(parent, borderStyle | style | orientation); + if (value != null) + text.setText(value); + text.setForeground(colors.getForeground()); + text.setBackground(colors.getBackground()); + text.addFocusListener(visibilityHandler); + return text; + } + + /** + * Creates a tree widget as a part of the form. + * + * @param parent + * the tree parent + * @param style + * the tree style + * @return the tree widget + */ + public Tree createTree(Composite parent, int style) { + Tree tree = new Tree(parent, borderStyle | style | orientation); + adapt(tree, false, false); + // hookDeleteListener(tree); + return tree; + } + + /** + * Creates a scrolled form widget in the provided parent. If you do not + * require scrolling because there is already a scrolled composite up the + * parent chain, use 'createForm' instead. + * + * @param parent + * the scrolled form parent + * @return the form that can scroll itself + * @see #createForm + */ + public ScrolledComposite createScrolledForm(Composite parent) { + ScrolledComposite form = new ScrolledComposite(parent, SWT.V_SCROLL + | SWT.H_SCROLL | orientation); + form.setExpandHorizontal(true); + form.setExpandVertical(true); + form.setBackground(colors.getBackground()); + form.setForeground(colors.getColor(IFormColors.TITLE)); + form.setFont(JFaceResources.getHeaderFont()); + return form; + } + + /** + * Creates a form widget in the provided parent. Note that this widget does + * not scroll its content, so make sure there is a scrolled composite up the + * parent chain. If you require scrolling, use 'createScrolledForm' instead. + * + * @param parent + * the form parent + * @return the form that does not scroll + * @see #createScrolledForm + */ +// public Form createForm(Composite parent) { +// Form formContent = new Form(parent, orientation); +// formContent.setBackground(colors.getBackground()); +// formContent.setForeground(colors.getColor(IFormColors.TITLE)); +// formContent.setFont(JFaceResources.getHeaderFont()); +// return formContent; +// } + + /** + * Takes advantage of the gradients and other capabilities to decorate the + * form heading using colors computed based on the current skin and + * operating system. + * + * @param form + * the form to decorate + */ + +// public void decorateFormHeading(Form form) { +// Color top = colors.getColor(IFormColors.H_GRADIENT_END); +// Color bot = colors.getColor(IFormColors.H_GRADIENT_START); +// form.setTextBackground(new Color[] { top, bot }, new int[] { 100 }, +// true); +// form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE1, colors +// .getColor(IFormColors.H_BOTTOM_KEYLINE1)); +// form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE2, colors +// .getColor(IFormColors.H_BOTTOM_KEYLINE2)); +// form.setHeadColor(IFormColors.H_HOVER_LIGHT, colors +// .getColor(IFormColors.H_HOVER_LIGHT)); +// form.setHeadColor(IFormColors.H_HOVER_FULL, colors +// .getColor(IFormColors.H_HOVER_FULL)); +// form.setHeadColor(IFormColors.TB_TOGGLE, colors +// .getColor(IFormColors.TB_TOGGLE)); +// form.setHeadColor(IFormColors.TB_TOGGLE_HOVER, colors +// .getColor(IFormColors.TB_TOGGLE_HOVER)); +// form.setSeparatorVisible(true); +// } + + /** + * Creates a scrolled page book widget as a part of the form. + * + * @param parent + * the page book parent + * @param style + * the text style + * @return the scrolled page book widget + */ +// public ScrolledPageBook createPageBook(Composite parent, int style) { +// ScrolledPageBook book = new ScrolledPageBook(parent, style +// | orientation); +// adapt(book, true, true); +// book.setMenu(parent.getMenu()); +// return book; +// } + + /** + * Disposes the toolkit. + */ + public void dispose() { + if (isDisposed) { + return; + } + isDisposed = true; + if (colors.isShared() == false) { + colors.dispose(); + colors = null; + } + boldFontHolder.dispose(); + } + + /** + * Returns the hyperlink group that manages hyperlinks for this toolkit. + * + * @return the hyperlink group + */ +// public HyperlinkGroup getHyperlinkGroup() { +// return hyperlinkGroup; +// } + + /** + * Sets the background color for the entire toolkit. The method delegates + * the call to the FormColors object and also updates the hyperlink group so + * that hyperlinks and other objects are in sync. + * + * @param bg + * the new background color + */ + public void setBackground(Color bg) { +// hyperlinkGroup.setBackground(bg); + colors.setBackground(bg); + } + + /** + * Refreshes the hyperlink colors by loading from JFace settings. + */ +// public void refreshHyperlinkColors() { +// hyperlinkGroup.initializeDefaultForegrounds(colors.getDisplay()); +// } + +// RAP [rh] paintBordersFor not useful as no GC to actually paint borders +// /** +// * Paints flat borders for widgets created by this toolkit within the +// * provided parent. Borders will not be painted if the global border style +// * is SWT.BORDER (i.e. if native borders are used). Call this method during +// * creation of a form composite to get the borders of its children painted. +// * Care should be taken when selection layout margins. At least one pixel +// * pargin width and height must be chosen to allow the toolkit to paint the +// * border on the parent around the widgets. +// *

+// * Borders are painted for some controls that are selected by the toolkit by +// * default. If a control needs a border but is not on its list, it is +// * possible to force border in the following way: +// * +// *

+//	 *
+//	 *
+//	 *
+//	 *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
+//	 *
+//	 *             or
+//	 *
+//	 *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
+//	 *
+//	 *
+//	 *
+//	 * 
+// * +// * @param parent +// * the parent that owns the children for which the border needs +// * to be painted. +// */ +// public void paintBordersFor(Composite parent) { +// // if (borderStyle == SWT.BORDER) +// // return; +// if (borderPainter == null) +// borderPainter = new BorderPainter(); +// parent.addPaintListener(borderPainter); +// } + + /** + * Returns the colors used by this toolkit. + * + * @return the color object + */ + public FormColors getColors() { + return colors; + } + + /** + * Returns the border style used for various widgets created by this + * toolkit. The intent of the toolkit is to create controls with styles that + * yield a 'flat' appearance. On systems where the native borders are + * already flat, we set the style to SWT.BORDER and don't paint the borders + * ourselves. Otherwise, the style is set to SWT.NULL, and borders are + * painted by the toolkit. + * + * @return the global border style + */ + public int getBorderStyle() { + return borderStyle; + } + + /** + * Returns the margin required around the children whose border is being + * painted by the toolkit using {@link #paintBordersFor(Composite)}. Since + * the border is painted around the controls on the parent, a number of + * pixels needs to be reserved for this border. For windowing systems where + * the native border is used, this margin is 0. + * + * @return the margin in the parent when children have their border painted + */ + public int getBorderMargin() { + return getBorderStyle() == SWT.BORDER ? 0 : 2; + } + + /** + * Sets the border style to be used when creating widgets. The toolkit + * chooses the correct style based on the platform but this value can be + * changed using this method. + * + * @param style + * SWT.BORDER or SWT.NULL + * @see #getBorderStyle + */ + public void setBorderStyle(int style) { + this.borderStyle = style; + } + + /** + * A utility method that ensures that the control is visible in the scrolled + * composite. The prerequisite for this method is that the control has a + * class that extends ScrolledComposite somewhere in the parent chain. If + * the control is partially or fully clipped, the composite is scrolled to + * set by setting the origin to the control origin. + * + * @param c + * the control to make visible + * @param verticalOnly + * if true, the scrolled composite will be + * scrolled only vertically if needed. Otherwise, the scrolled + * composite origin will be set to the control origin. + */ + public static void setControlVisible(Control c, boolean verticalOnly) { + ScrolledComposite scomp = FormUtil.getScrolledComposite(c); + if (scomp == null) + return; + Point location = FormUtil.getControlLocation(scomp, c); + scomp.setOrigin(location); + } + + private void initialize() { + initializeBorderStyle(); +// hyperlinkGroup = new HyperlinkGroup(colors.getDisplay()); +// hyperlinkGroup.setBackground(colors.getBackground()); + visibilityHandler = new VisibilityHandler(); + keyboardHandler = new KeyboardHandler(); + boldFontHolder = new BoldFontHolder(); + } + +// RAP [rh] revise detection of border style: can't ask OS here + private void initializeBorderStyle() { +// String osname = System.getProperty("os.name"); //$NON-NLS-1$ +// String osversion = System.getProperty("os.version"); //$NON-NLS-1$ +// if (osname.startsWith("Windows") && "5.1".compareTo(osversion) <= 0) { //$NON-NLS-1$ //$NON-NLS-2$ +// // Skinned widgets used on newer Windows (e.g. XP (5.1), Vista +// // (6.0)) +// // Check for Windows Classic. If not used, set the style to BORDER +// RGB rgb = colors.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); +// if (rgb.red != 212 || rgb.green != 208 || rgb.blue != 200) +// borderStyle = SWT.BORDER; +// } else if (osname.startsWith("Mac")) //$NON-NLS-1$ +// borderStyle = SWT.BORDER; + + borderStyle = SWT.BORDER; + } + + /** + * Returns the orientation that all the widgets created by this toolkit will + * inherit, if set. Can be SWT.NULL, + * SWT.LEFT_TO_RIGHT and SWT.RIGHT_TO_LEFT. + * + * @return orientation style for this toolkit, or SWT.NULL if + * not set. The default orientation is inherited from the Window + * default orientation. + * @see org.eclipse.jface.window.Window#getDefaultOrientation() + */ + + public int getOrientation() { + return orientation; + } + + /** + * Sets the orientation that all the widgets created by this toolkit will + * inherit. Can be SWT.NULL, SWT.LEFT_TO_RIGHT + * and SWT.RIGHT_TO_LEFT. + * + * @param orientation + * style for this toolkit. + */ + + public void setOrientation(int orientation) { + this.orientation = orientation; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java new file mode 100644 index 000000000..76e3f1104 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java @@ -0,0 +1,522 @@ +package org.argeo.cms.ui.eclipse.forms; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.MouseEvent; +//import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.FontMetrics; +import org.eclipse.swt.graphics.GC; +//import org.eclipse.swt.graphics.Image; +//import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Layout; +//import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Text; +//import org.eclipse.ui.forms.widgets.ColumnLayout; +//import org.eclipse.ui.forms.widgets.Form; +//import org.eclipse.ui.forms.widgets.FormText; +//import org.eclipse.ui.forms.widgets.FormToolkit; +//import org.eclipse.ui.forms.widgets.ILayoutExtension; +// +//import com.ibm.icu.text.BreakIterator; + +public class FormUtil { + public static final String PLUGIN_ID = "org.eclipse.ui.forms"; //$NON-NLS-1$ + + static final int H_SCROLL_INCREMENT = 5; + + static final int V_SCROLL_INCREMENT = 64; + + public static final String DEBUG = PLUGIN_ID + "/debug"; //$NON-NLS-1$ + + public static final String DEBUG_TEXT = DEBUG + "/text"; //$NON-NLS-1$ + public static final String DEBUG_TEXTSIZE = DEBUG + "/textsize"; //$NON-NLS-1$ + + public static final String DEBUG_FOCUS = DEBUG + "/focus"; //$NON-NLS-1$ + + public static final String FOCUS_SCROLLING = "focusScrolling"; //$NON-NLS-1$ + + public static final String IGNORE_BODY = "__ignore_body__"; //$NON-NLS-1$ + + public static Text createText(Composite parent, String label, + FormToolkit factory) { + return createText(parent, label, factory, 1); + } + + public static Text createText(Composite parent, String label, + FormToolkit factory, int span) { + factory.createLabel(parent, label); + Text text = factory.createText(parent, ""); //$NON-NLS-1$ + int hfill = span == 1 ? GridData.FILL_HORIZONTAL + : GridData.HORIZONTAL_ALIGN_FILL; + GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER); + gd.horizontalSpan = span; + text.setLayoutData(gd); + return text; + } + + public static Text createText(Composite parent, String label, + FormToolkit factory, int span, int style) { + Label l = factory.createLabel(parent, label); + if ((style & SWT.MULTI) != 0) { + GridData gd = new GridData(GridData.VERTICAL_ALIGN_BEGINNING); + l.setLayoutData(gd); + } + Text text = factory.createText(parent, "", style); //$NON-NLS-1$ + int hfill = span == 1 ? GridData.FILL_HORIZONTAL + : GridData.HORIZONTAL_ALIGN_FILL; + GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER); + gd.horizontalSpan = span; + text.setLayoutData(gd); + return text; + } + + public static Text createText(Composite parent, FormToolkit factory, + int span) { + Text text = factory.createText(parent, ""); //$NON-NLS-1$ + int hfill = span == 1 ? GridData.FILL_HORIZONTAL + : GridData.HORIZONTAL_ALIGN_FILL; + GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER); + gd.horizontalSpan = span; + text.setLayoutData(gd); + return text; + } + + public static int computeMinimumWidth(GC gc, String text) { +// BreakIterator wb = BreakIterator.getWordInstance(); +// wb.setText(text); +// int last = 0; +// +// int width = 0; +// +// for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) { +// String word = text.substring(last, loc); +// Point extent = gc.textExtent(word); +// width = Math.max(width, extent.x); +// last = loc; +// } +// String lastWord = text.substring(last); +// Point extent = gc.textExtent(lastWord); +// width = Math.max(width, extent.x); +// return width; + return 0; + } + + public static Point computeWrapSize(GC gc, String text, int wHint) { +// BreakIterator wb = BreakIterator.getWordInstance(); +// wb.setText(text); + FontMetrics fm = gc.getFontMetrics(); + int lineHeight = fm.getHeight(); + + int saved = 0; + int last = 0; + int height = lineHeight; + int maxWidth = 0; +// for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) { +// String word = text.substring(saved, loc); +// Point extent = gc.textExtent(word); +// if (extent.x > wHint) { +// // overflow +// saved = last; +// height += extent.y; +// // switch to current word so maxWidth will accommodate very long single words +// word = text.substring(last, loc); +// extent = gc.textExtent(word); +// } +// maxWidth = Math.max(maxWidth, extent.x); +// last = loc; +// } + /* + * Correct the height attribute in case it was calculated wrong due to wHint being less than maxWidth. + * The recursive call proved to be the only thing that worked in all cases. Some attempts can be made + * to estimate the height, but the algorithm needs to be run again to be sure. + */ + if (maxWidth > wHint) + return computeWrapSize(gc, text, maxWidth); + return new Point(maxWidth, height); + } + +// RAP [rh] paintWrapText unnecessary +// public static void paintWrapText(GC gc, String text, Rectangle bounds) { +// paintWrapText(gc, text, bounds, false); +// } + +// RAP [rh] paintWrapText unnecessary +// public static void paintWrapText(GC gc, String text, Rectangle bounds, +// boolean underline) { +// BreakIterator wb = BreakIterator.getWordInstance(); +// wb.setText(text); +// FontMetrics fm = gc.getFontMetrics(); +// int lineHeight = fm.getHeight(); +// int descent = fm.getDescent(); +// +// int saved = 0; +// int last = 0; +// int y = bounds.y; +// int width = bounds.width; +// +// for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) { +// String line = text.substring(saved, loc); +// Point extent = gc.textExtent(line); +// +// if (extent.x > width) { +// // overflow +// String prevLine = text.substring(saved, last); +// gc.drawText(prevLine, bounds.x, y, true); +// if (underline) { +// Point prevExtent = gc.textExtent(prevLine); +// int lineY = y + lineHeight - descent + 1; +// gc +// .drawLine(bounds.x, lineY, bounds.x + prevExtent.x, +// lineY); +// } +// +// saved = last; +// y += lineHeight; +// } +// last = loc; +// } +// // paint the last line +// String lastLine = text.substring(saved, last); +// gc.drawText(lastLine, bounds.x, y, true); +// if (underline) { +// int lineY = y + lineHeight - descent + 1; +// Point lastExtent = gc.textExtent(lastLine); +// gc.drawLine(bounds.x, lineY, bounds.x + lastExtent.x, lineY); +// } +// } + + public static ScrolledComposite getScrolledComposite(Control c) { + Composite parent = c.getParent(); + + while (parent != null) { + if (parent instanceof ScrolledComposite) { + return (ScrolledComposite) parent; + } + parent = parent.getParent(); + } + return null; + } + + public static void ensureVisible(Control c) { + ScrolledComposite scomp = getScrolledComposite(c); + if (scomp != null) { + Object data = scomp.getData(FOCUS_SCROLLING); + if (data == null || !data.equals(Boolean.FALSE)) + FormUtil.ensureVisible(scomp, c); + } + } + + public static void ensureVisible(ScrolledComposite scomp, Control control) { + // if the control is a FormText we do not need to scroll since it will + // ensure visibility of its segments as necessary +// if (control instanceof FormText) +// return; + Point controlSize = control.getSize(); + Point controlOrigin = getControlLocation(scomp, control); + ensureVisible(scomp, controlOrigin, controlSize); + } + + public static void ensureVisible(ScrolledComposite scomp, + Point controlOrigin, Point controlSize) { + Rectangle area = scomp.getClientArea(); + Point scompOrigin = scomp.getOrigin(); + + int x = scompOrigin.x; + int y = scompOrigin.y; + + // horizontal right, but only if the control is smaller + // than the client area + if (controlSize.x < area.width + && (controlOrigin.x + controlSize.x > scompOrigin.x + + area.width)) { + x = controlOrigin.x + controlSize.x - area.width; + } + // horizontal left - make sure the left edge of + // the control is showing + if (controlOrigin.x < x) { + if (controlSize.x < area.width) + x = controlOrigin.x + controlSize.x - area.width; + else + x = controlOrigin.x; + } + // vertical bottom + if (controlSize.y < area.height + && (controlOrigin.y + controlSize.y > scompOrigin.y + + area.height)) { + y = controlOrigin.y + controlSize.y - area.height; + } + // vertical top - make sure the top of + // the control is showing + if (controlOrigin.y < y) { + if (controlSize.y < area.height) + y = controlOrigin.y + controlSize.y - area.height; + else + y = controlOrigin.y; + } + + if (scompOrigin.x != x || scompOrigin.y != y) { + // scroll to reveal + scomp.setOrigin(x, y); + } + } + + public static void ensureVisible(ScrolledComposite scomp, Control control, + MouseEvent e) { + Point controlOrigin = getControlLocation(scomp, control); + int rX = controlOrigin.x + e.x; + int rY = controlOrigin.y + e.y; + Rectangle area = scomp.getClientArea(); + Point scompOrigin = scomp.getOrigin(); + + int x = scompOrigin.x; + int y = scompOrigin.y; + // System.out.println("Ensure: area="+area+", origin="+scompOrigin+", + // cloc="+controlOrigin+", csize="+controlSize+", x="+x+", y="+y); + + // horizontal right + if (rX > scompOrigin.x + area.width) { + x = rX - area.width; + } + // horizontal left + else if (rX < x) { + x = rX; + } + // vertical bottom + if (rY > scompOrigin.y + area.height) { + y = rY - area.height; + } + // vertical top + else if (rY < y) { + y = rY; + } + + if (scompOrigin.x != x || scompOrigin.y != y) { + // scroll to reveal + scomp.setOrigin(x, y); + } + } + + public static Point getControlLocation(ScrolledComposite scomp, + Control control) { + int x = 0; + int y = 0; + Control content = scomp.getContent(); + Control currentControl = control; + for (;;) { + if (currentControl == content) + break; + Point location = currentControl.getLocation(); + // if (location.x > 0) + // x += location.x; + // if (location.y > 0) + // y += location.y; + x += location.x; + y += location.y; + currentControl = currentControl.getParent(); + } + return new Point(x, y); + } + + static void scrollVertical(ScrolledComposite scomp, boolean up) { + scroll(scomp, 0, up ? -V_SCROLL_INCREMENT : V_SCROLL_INCREMENT); + } + + static void scrollHorizontal(ScrolledComposite scomp, boolean left) { + scroll(scomp, left ? -H_SCROLL_INCREMENT : H_SCROLL_INCREMENT, 0); + } + + static void scrollPage(ScrolledComposite scomp, boolean up) { + Rectangle clientArea = scomp.getClientArea(); + int increment = up ? -clientArea.height : clientArea.height; + scroll(scomp, 0, increment); + } + + static void scroll(ScrolledComposite scomp, int xoffset, int yoffset) { + Point origin = scomp.getOrigin(); + Point contentSize = scomp.getContent().getSize(); + int xorigin = origin.x + xoffset; + int yorigin = origin.y + yoffset; + xorigin = Math.max(xorigin, 0); + xorigin = Math.min(xorigin, contentSize.x - 1); + yorigin = Math.max(yorigin, 0); + yorigin = Math.min(yorigin, contentSize.y - 1); + scomp.setOrigin(xorigin, yorigin); + } + +// RAP [rh] FormUtil#updatePageIncrement: empty implementation + public static void updatePageIncrement(ScrolledComposite scomp) { +// ScrollBar vbar = scomp.getVerticalBar(); +// if (vbar != null) { +// Rectangle clientArea = scomp.getClientArea(); +// int increment = clientArea.height - 5; +// vbar.setPageIncrement(increment); +// } +// ScrollBar hbar = scomp.getHorizontalBar(); +// if (hbar != null) { +// Rectangle clientArea = scomp.getClientArea(); +// int increment = clientArea.width - 5; +// hbar.setPageIncrement(increment); +// } + } + + public static void processKey(int keyCode, Control c) { + if (c.isDisposed()) { + return; + } + ScrolledComposite scomp = FormUtil.getScrolledComposite(c); + if (scomp != null) { + if (c instanceof Combo) + return; + switch (keyCode) { + case SWT.ARROW_DOWN: + if (scomp.getData("novarrows") == null) //$NON-NLS-1$ + FormUtil.scrollVertical(scomp, false); + break; + case SWT.ARROW_UP: + if (scomp.getData("novarrows") == null) //$NON-NLS-1$ + FormUtil.scrollVertical(scomp, true); + break; + case SWT.ARROW_LEFT: + FormUtil.scrollHorizontal(scomp, true); + break; + case SWT.ARROW_RIGHT: + FormUtil.scrollHorizontal(scomp, false); + break; + case SWT.PAGE_UP: + FormUtil.scrollPage(scomp, true); + break; + case SWT.PAGE_DOWN: + FormUtil.scrollPage(scomp, false); + break; + } + } + } + + public static boolean isWrapControl(Control c) { + if ((c.getStyle() & SWT.WRAP) != 0) + return true; + if (c instanceof Composite) { + return false; +// return ((Composite) c).getLayout() instanceof ILayoutExtension; + } + return false; + } + + public static int getWidthHint(int wHint, Control c) { + boolean wrap = isWrapControl(c); + return wrap ? wHint : SWT.DEFAULT; + } + + public static int getHeightHint(int hHint, Control c) { + if (c instanceof Composite) { + Layout layout = ((Composite) c).getLayout(); +// if (layout instanceof ColumnLayout) +// return hHint; + } + return SWT.DEFAULT; + } + + public static int computeMinimumWidth(Control c, boolean changed) { + if (c instanceof Composite) { + Layout layout = ((Composite) c).getLayout(); +// if (layout instanceof ILayoutExtension) +// return ((ILayoutExtension) layout).computeMinimumWidth( +// (Composite) c, changed); + } + return c.computeSize(FormUtil.getWidthHint(5, c), SWT.DEFAULT, changed).x; + } + + public static int computeMaximumWidth(Control c, boolean changed) { + if (c instanceof Composite) { + Layout layout = ((Composite) c).getLayout(); +// if (layout instanceof ILayoutExtension) +// return ((ILayoutExtension) layout).computeMaximumWidth( +// (Composite) c, changed); + } + return c.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed).x; + } + +// public static Form getForm(Control c) { +// Composite parent = c.getParent(); +// while (parent != null) { +// if (parent instanceof Form) { +// return (Form) parent; +// } +// parent = parent.getParent(); +// } +// return null; +// } + +// RAP [rh] FormUtil#createAlphaMashImage unnecessary +// public static Image createAlphaMashImage(Device device, Image srcImage) { +// Rectangle bounds = srcImage.getBounds(); +// int alpha = 0; +// int calpha = 0; +// ImageData data = srcImage.getImageData(); +// // Create a new image with alpha values alternating +// // between fully transparent (0) and fully opaque (255). +// // This image will show the background through the +// // transparent pixels. +// for (int i = 0; i < bounds.height; i++) { +// // scan line +// alpha = calpha; +// for (int j = 0; j < bounds.width; j++) { +// // column +// data.setAlpha(j, i, alpha); +// alpha = alpha == 255 ? 0 : 255; +// } +// calpha = calpha == 255 ? 0 : 255; +// } +// return new Image(device, data); +// } + + public static boolean mnemonicMatch(String text, char key) { + char mnemonic = findMnemonic(text); + if (mnemonic == '\0') + return false; + return Character.toUpperCase(key) == Character.toUpperCase(mnemonic); + } + + private static char findMnemonic(String string) { + int index = 0; + int length = string.length(); + do { + while (index < length && string.charAt(index) != '&') + index++; + if (++index >= length) + return '\0'; + if (string.charAt(index) != '&') + return string.charAt(index); + index++; + } while (index < length); + return '\0'; + } + + public static void setFocusScrollingEnabled(Control c, boolean enabled) { + ScrolledComposite scomp = null; + + if (c instanceof ScrolledComposite) + scomp = (ScrolledComposite)c; + else + scomp = getScrolledComposite(c); + if (scomp!=null) + scomp.setData(FormUtil.FOCUS_SCROLLING, enabled?null:Boolean.FALSE); + } + + // RAP [rh] FormUtil#setAntialias unnecessary +// public static void setAntialias(GC gc, int style) { +// if (!gc.getAdvanced()) { +// gc.setAdvanced(true); +// if (!gc.getAdvanced()) +// return; +// } +// gc.setAntialias(style); +// } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java new file mode 100644 index 000000000..cf0e5d35e --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java @@ -0,0 +1,102 @@ +package org.argeo.cms.ui.eclipse.forms; + +/** + * A place to hold all the color constants used in the forms package. + * + * @since 1.0 + */ + +public interface IFormColors { + /** + * A prefix for all the keys. + */ + String PREFIX = "org.eclipse.ui.forms."; //$NON-NLS-1$ + /** + * Key for the form title foreground color. + */ + String TITLE = PREFIX + "TITLE"; //$NON-NLS-1$ + + /** + * A prefix for the header color constants. + */ + String H_PREFIX = PREFIX + "H_"; //$NON-NLS-1$ + /* + * A prefix for the section title bar color constants. + */ + String TB_PREFIX = PREFIX + "TB_"; //$NON-NLS-1$ + /** + * Key for the form header background gradient ending color. + */ + String H_GRADIENT_END = H_PREFIX + "GRADIENT_END"; //$NON-NLS-1$ + + /** + * Key for the form header background gradient starting color. + * + */ + String H_GRADIENT_START = H_PREFIX + "GRADIENT_START"; //$NON-NLS-1$ + /** + * Key for the form header bottom keyline 1 color. + * + */ + String H_BOTTOM_KEYLINE1 = H_PREFIX + "BOTTOM_KEYLINE1"; //$NON-NLS-1$ + /** + * Key for the form header bottom keyline 2 color. + * + */ + String H_BOTTOM_KEYLINE2 = H_PREFIX + "BOTTOM_KEYLINE2"; //$NON-NLS-1$ + /** + * Key for the form header light hover color. + * + */ + String H_HOVER_LIGHT = H_PREFIX + "H_HOVER_LIGHT"; //$NON-NLS-1$ + /** + * Key for the form header full hover color. + * + */ + String H_HOVER_FULL = H_PREFIX + "H_HOVER_FULL"; //$NON-NLS-1$ + + /** + * Key for the tree/table border color. + */ + String BORDER = PREFIX + "BORDER"; //$NON-NLS-1$ + + /** + * Key for the section separator color. + */ + String SEPARATOR = PREFIX + "SEPARATOR"; //$NON-NLS-1$ + + /** + * Key for the section title bar background. + */ + String TB_BG = TB_PREFIX + "BG"; //$NON-NLS-1$ + + /** + * Key for the section title bar foreground. + */ + String TB_FG = TB_PREFIX + "FG"; //$NON-NLS-1$ + + /** + * Key for the section title bar gradient. + * @deprecated Since 3.3, this color is not used any more. The + * tool bar gradient is created starting from {@link #TB_BG} to + * the section background color. + */ + String TB_GBG = TB_BG; + + /** + * Key for the section title bar border. + */ + String TB_BORDER = TB_PREFIX + "BORDER"; //$NON-NLS-1$ + + /** + * Key for the section toggle color. Since 3.1, this color is used for all + * section styles. + */ + String TB_TOGGLE = TB_PREFIX + "TOGGLE"; //$NON-NLS-1$ + + /** + * Key for the section toggle hover color. + * + */ + String TB_TOGGLE_HOVER = TB_PREFIX + "TOGGLE_HOVER"; //$NON-NLS-1$ +} \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java new file mode 100644 index 000000000..954cc0372 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java @@ -0,0 +1,108 @@ +package org.argeo.cms.ui.eclipse.forms; + +/** + * Classes that implement this interface can be added to the managed form and + * take part in the form life cycle. The part is initialized with the form and + * will be asked to accept focus. The part can receive form input and can elect + * to do something according to it (for example, select an object that matches + * the input). + *

+ * The form part has two 'out of sync' states in respect to the model(s) that + * feed the form: dirty and stale. When a part is dirty, it + * means that the user interacted with it and now its widgets contain state that + * is newer than the model. In order to sync up with the model, 'commit' needs + * to be called. In contrast, the model can change 'under' the form (as a result + * of some actions outside the form), resulting in data in the model being + * 'newer' than the content presented in the form. A 'stale' form part is + * brought in sync with the model by calling 'refresh'. The part is responsible + * for notifying the form when one of these states change in the part. The form + * reserves the right to handle this notification in the most appropriate way + * for the situation (for example, if the form is in a page of the multi-page + * editor, it may do nothing for stale parts if the page is currently not + * showing). + *

+ * When the form is disposed, each registered part is disposed as well. Parts + * are responsible for releasing any system resources they created and for + * removing themselves as listeners from all event providers. + * + * @see IManagedForm + * @since 1.0 + * + */ +public interface IFormPart { + /** + * Initializes the part. + * + * @param form + * the managed form that manages the part + */ + void initialize(IManagedForm form); + + /** + * Disposes the part allowing it to release allocated resources. + */ + void dispose(); + + /** + * Returns true if the part has been modified with respect to the data + * loaded from the model. + * + * @return true if the part has been modified with respect to the data + * loaded from the model + */ + boolean isDirty(); + + /** + * If part is displaying information loaded from a model, this method + * instructs it to commit the new (modified) data back into the model. + * + * @param onSave + * indicates if commit is called during 'save' operation or for + * some other reason (for example, if form is contained in a + * wizard or a multi-page editor and the user is about to leave + * the page). + */ + void commit(boolean onSave); + + /** + * Notifies the part that an object has been set as overall form's input. + * The part can elect to react by revealing or selecting the object, or do + * nothing if not applicable. + * + * @return true if the part has selected and revealed the + * input object, false otherwise. + */ + boolean setFormInput(Object input); + + /** + * Instructs form part to transfer focus to the widget that should has focus + * in that part. The method can do nothing (if it has no widgets capable of + * accepting focus). + */ + void setFocus(); + + /** + * Tests whether the form part is stale and needs refreshing. Parts can + * receive notification from models that will make their content stale, but + * may need to delay refreshing to improve performance (for example, there + * is no need to immediately refresh a part on a form that is current on a + * hidden page). + *

+ * It is important to differentiate 'stale' and 'dirty' states. Part is + * 'dirty' if user interacted with its editable widgets and changed the + * values. In contrast, part is 'stale' when the data it presents in the + * widgets has been changed in the model without direct user interaction. + * + * @return true if the part needs refreshing, + * false otherwise. + */ + boolean isStale(); + + /** + * Refreshes the part completely from the information freshly obtained from + * the model. The method will not be called if the part is not stale. + * Otherwise, the part is responsible for clearing the 'stale' flag after + * refreshing itself. + */ + void refresh(); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java new file mode 100644 index 000000000..490d3a303 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java @@ -0,0 +1,175 @@ +package org.argeo.cms.ui.eclipse.forms; + +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.custom.ScrolledComposite; +//import org.eclipse.ui.forms.widgets.FormToolkit; +//import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Managed form wraps a form widget and adds life cycle methods for form parts. + * A form part is a portion of the form that participates in form life cycle + * events. + *

+ * There is no 1/1 mapping between widgets and form parts. A widget like Section + * can be a part by itself, but a number of widgets can gather around one form + * part. + *

+ * This interface should not be extended or implemented. New form instances + * should be created using ManagedForm. + * + * @see ManagedForm + * @since 1.0 + * @noimplement This interface is not intended to be implemented by clients. + * @noextend This interface is not intended to be extended by clients. + */ +public interface IManagedForm { + /** + * Initializes the form by looping through the managed parts and + * initializing them. Has no effect if already called once. + */ + public void initialize(); + + /** + * Returns the toolkit used by this form. + * + * @return the toolkit + */ + public FormToolkit getToolkit(); + + /** + * Returns the form widget managed by this form. + * + * @return the form widget + */ + public ScrolledComposite getForm(); + + /** + * Reflows the form as a result of the layout change. + * + * @param changed + * if true, discard cached layout information + */ + public void reflow(boolean changed); + + /** + * A part can use this method to notify other parts that implement + * IPartSelectionListener about selection changes. + * + * @param part + * the part that broadcasts the selection + * @param selection + * the selection in the part + */ + public void fireSelectionChanged(IFormPart part, ISelection selection); + + /** + * Returns all the parts currently managed by this form. + * + * @return the managed parts + */ + IFormPart[] getParts(); + + /** + * Adds the new part to the form. + * + * @param part + * the part to add + */ + void addPart(IFormPart part); + + /** + * Removes the part from the form. + * + * @param part + * the part to remove + */ + void removePart(IFormPart part); + + /** + * Sets the input of this page to the provided object. + * + * @param input + * the new page input + * @return true if the form contains this object, + * false otherwise. + */ + boolean setInput(Object input); + + /** + * Returns the current page input. + * + * @return page input object or null if not applicable. + */ + Object getInput(); + + /** + * Tests if form is dirty. A managed form is dirty if at least one managed + * part is dirty. + * + * @return true if at least one managed part is dirty, + * false otherwise. + */ + boolean isDirty(); + + /** + * Notifies the form that the dirty state of one of its parts has changed. + * The global dirty state of the form can be obtained by calling 'isDirty'. + * + * @see #isDirty + */ + void dirtyStateChanged(); + + /** + * Commits the dirty form. All pending changes in the widgets are flushed + * into the model. + * + * @param onSave + */ + void commit(boolean onSave); + + /** + * Tests if form is stale. A managed form is stale if at least one managed + * part is stale. This can happen when the underlying model changes, + * resulting in the presentation of the part being out of sync with the + * model and needing refreshing. + * + * @return true if the form is stale, false + * otherwise. + */ + boolean isStale(); + + /** + * Notifies the form that the stale state of one of its parts has changed. + * The global stale state of the form can be obtained by calling 'isStale'. + */ + void staleStateChanged(); + + /** + * Refreshes the form by refreshing every part that is stale. + */ + void refresh(); + + /** + * Sets the container that owns this form. Depending on the context, the + * container may be wizard, editor page, editor etc. + * + * @param container + * the container of this form + */ + void setContainer(Object container); + + /** + * Returns the container of this form. + * + * @return the form container + */ + Object getContainer(); + + /** + * Returns the message manager that will keep track of messages in this + * form. + * + * @return the message manager instance + */ +// IMessageManager getMessageManager(); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java new file mode 100644 index 000000000..0f557d41f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java @@ -0,0 +1,23 @@ +package org.argeo.cms.ui.eclipse.forms; + +import org.eclipse.jface.viewers.ISelection; + +/** + * Form parts can implement this interface if they want to be + * notified when another part on the same form changes selection + * state. + * + * @see IFormPart + * @since 1.0 + */ +public interface IPartSelectionListener { + /** + * Called when the provided part has changed selection state. + * + * @param part + * the selection source + * @param selection + * the new selection + */ + public void selectionChanged(IFormPart part, ISelection selection); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java new file mode 100644 index 000000000..4140465a1 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java @@ -0,0 +1,323 @@ +package org.argeo.cms.ui.eclipse.forms; + +import java.util.Vector; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.widgets.Composite; +//import org.eclipse.ui.forms.widgets.FormToolkit; +//import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Managed form wraps a form widget and adds life cycle methods for form parts. + * A form part is a portion of the form that participates in form life cycle + * events. + *

+ * There is requirement for 1/1 mapping between widgets and form parts. A widget + * like Section can be a part by itself, but a number of widgets can join around + * one form part. + *

+ * Note to developers: this class is left public to allow its use beyond the + * original intention (inside a multi-page editor's page). You should limit the + * use of this class to make new instances inside a form container (wizard page, + * dialog etc.). Clients that need access to the class should not do it + * directly. Instead, they should do it through IManagedForm interface as much + * as possible. + * + * @since 1.0 + */ +public class ManagedForm implements IManagedForm { + private Object input; + + private ScrolledComposite form; + + private FormToolkit toolkit; + + private Object container; + + private boolean ownsToolkit; + + private boolean initialized; + + private Vector parts = new Vector(); + + /** + * Creates a managed form in the provided parent. Form toolkit and widget + * will be created and owned by this object. + * + * @param parent + * the parent widget + */ + public ManagedForm(Composite parent) { + toolkit = new FormToolkit(parent.getDisplay()); + ownsToolkit = true; + form = toolkit.createScrolledForm(parent); + + } + + /** + * Creates a managed form that will use the provided toolkit and + * + * @param toolkit + * @param form + */ + public ManagedForm(FormToolkit toolkit, ScrolledComposite form) { + this.form = form; + this.toolkit = toolkit; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#addPart(org.eclipse.ui.forms.IFormPart) + */ + public void addPart(IFormPart part) { + parts.add(part); + part.initialize(this); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#removePart(org.eclipse.ui.forms.IFormPart) + */ + public void removePart(IFormPart part) { + parts.remove(part); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#getParts() + */ + public IFormPart[] getParts() { + return (IFormPart[]) parts.toArray(new IFormPart[parts.size()]); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#getToolkit() + */ + public FormToolkit getToolkit() { + return toolkit; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#getForm() + */ + public ScrolledComposite getForm() { + return form; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#reflow(boolean) + */ + public void reflow(boolean changed) { +// form.reflow(changed); + } + + /** + * A part can use this method to notify other parts that implement + * IPartSelectionListener about selection changes. + * + * @param part + * the part that broadcasts the selection + * @param selection + * the selection in the part + * @see IPartSelectionListener + */ + public void fireSelectionChanged(IFormPart part, ISelection selection) { + for (int i = 0; i < parts.size(); i++) { + IFormPart cpart = (IFormPart) parts.get(i); + if (part.equals(cpart)) + continue; +// if (cpart instanceof IPartSelectionListener) { +// ((IPartSelectionListener) cpart).selectionChanged(part, +// selection); +// } + } + } + + /** + * Initializes the form by looping through the managed parts and + * initializing them. Has no effect if already called once. + */ + public void initialize() { + if (initialized) + return; + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + part.initialize(this); + } + initialized = true; + } + + /** + * Disposes all the parts in this form. + */ + public void dispose() { + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + part.dispose(); + } + if (ownsToolkit) { + toolkit.dispose(); + } + } + + /** + * Refreshes the form by refreshes all the stale parts. Since 3.1, this + * method is performed on a UI thread when called from another thread so it + * is not needed to wrap the call in Display.syncExec or + * asyncExec. + */ + public void refresh() { + Thread t = Thread.currentThread(); + Thread dt = toolkit.getColors().getDisplay().getThread(); + if (t.equals(dt)) + doRefresh(); + else { + toolkit.getColors().getDisplay().asyncExec(new Runnable() { + public void run() { + doRefresh(); + } + }); + } + } + + private void doRefresh() { + int nrefreshed = 0; + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + if (part.isStale()) { + part.refresh(); + nrefreshed++; + } + } +// if (nrefreshed > 0) +// form.reflow(true); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#commit(boolean) + */ + public void commit(boolean onSave) { + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + if (part.isDirty()) + part.commit(onSave); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#setInput(java.lang.Object) + */ + public boolean setInput(Object input) { + boolean pageResult = false; + + this.input = input; + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + boolean result = part.setFormInput(input); + if (result) + pageResult = true; + } + return pageResult; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#getInput() + */ + public Object getInput() { + return input; + } + + /** + * Transfers the focus to the first form part. + */ + public void setFocus() { + if (parts.size() > 0) { + IFormPart part = (IFormPart) parts.get(0); + part.setFocus(); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#isDirty() + */ + public boolean isDirty() { + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + if (part.isDirty()) + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#isStale() + */ + public boolean isStale() { + for (int i = 0; i < parts.size(); i++) { + IFormPart part = (IFormPart) parts.get(i); + if (part.isStale()) + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#dirtyStateChanged() + */ + public void dirtyStateChanged() { + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#staleStateChanged() + */ + public void staleStateChanged() { + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#getContainer() + */ + public Object getContainer() { + return container; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.forms.IManagedForm#setContainer(java.lang.Object) + */ + public void setContainer(Object container) { + this.container = container; + } + + /* (non-Javadoc) + * @see org.eclipse.ui.forms.IManagedForm#getMessageManager() + */ +// public IMessageManager getMessageManager() { +// return form.getMessageManager(); +// } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java new file mode 100644 index 000000000..484dae842 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java @@ -0,0 +1,85 @@ +package org.argeo.cms.ui.eclipse.forms.editor; + +import org.argeo.cms.ui.eclipse.forms.FormToolkit; +import org.eclipse.jface.dialogs.IPageChangeProvider; + +/** + * This class forms a base of multi-page form editors that typically use one or + * more pages with forms and one page for raw source of the editor input. + *

+ * Pages are added 'lazily' i.e. adding a page reserves a tab for it but does + * not cause the page control to be created. Page control is created when an + * attempt is made to select the page in question. This allows editors with + * several tabs and complex pages to open quickly. + *

+ * Subclasses should extend this class and implement addPages + * method. One of the two addPage methods should be called to + * contribute pages to the editor. One adds complete (standalone) editors as + * nested tabs. These editors will be created right away and will be hooked so + * that key bindings, selection service etc. is compatible with the one for the + * standalone case. The other method adds classes that implement + * IFormPage interface. These pages will be created lazily and + * they will share the common key binding and selection service. Since 3.1, + * FormEditor is a page change provider. It allows listeners to attach to it and + * get notified when pages are changed. This new API in JFace allows dynamic + * help to update on page changes. + * + * @since 1.0 + */ +// RAP [if] As RAP is still using workbench 3.4, the implementation of +// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code +// with the adoption of workbench > 3.5 +//public abstract class FormEditor extends MultiPageEditorPart { +public abstract class FormEditor implements + IPageChangeProvider { + private FormToolkit formToolkit; + + +public FormToolkit getToolkit() { + return formToolkit; + } + +public void editorDirtyStateChanged() { + +} + +public FormPage getActivePageInstance() { + return null; +} + + // RAP [if] As RAP is still using workbench 3.4, the implementation of +// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code +// with the adoption of workbench > 3.5 +// private ListenerList pageListeners = new ListenerList(); +// +// /* +// * (non-Javadoc) +// * +// * @see org.eclipse.jface.dialogs.IPageChangeProvider#addPageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener) +// */ +// public void addPageChangedListener(IPageChangedListener listener) { +// pageListeners.add(listener); +// } +// +// /* +// * (non-Javadoc) +// * +// * @see org.eclipse.jface.dialogs.IPageChangeProvider#removePageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener) +// */ +// public void removePageChangedListener(IPageChangedListener listener) { +// pageListeners.remove(listener); +// } +// +// private void firePageChanged(final PageChangedEvent event) { +// Object[] listeners = pageListeners.getListeners(); +// for (int i = 0; i < listeners.length; ++i) { +// final IPageChangedListener l = (IPageChangedListener) listeners[i]; +// SafeRunnable.run(new SafeRunnable() { +// public void run() { +// l.pageChanged(event); +// } +// }); +// } +// } +// RAPEND [if] +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java new file mode 100644 index 000000000..a788412db --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java @@ -0,0 +1,276 @@ +package org.argeo.cms.ui.eclipse.forms.editor; +import org.argeo.cms.ui.eclipse.forms.IManagedForm; +import org.argeo.cms.ui.eclipse.forms.ManagedForm; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +/** + * A base class that all pages that should be added to FormEditor must subclass. + * Form page has an instance of PageForm that extends managed form. Subclasses + * should override method 'createFormContent(ManagedForm)' to fill the form with + * content. Note that page itself can be loaded lazily (on first open). + * Consequently, the call to create the form content can come after the editor + * has been opened for a while (in fact, it is possible to open and close the + * editor and never create the form because no attempt has been made to show the + * page). + * + * @since 1.0 + */ +public class FormPage implements IFormPage { + private FormEditor editor; + private PageForm mform; + private int index; + private String id; + + private String partName; + + + + public void setPartName(String partName) { + this.partName = partName; + } + private static class PageForm extends ManagedForm { + public PageForm(FormPage page, ScrolledComposite form) { + super(page.getEditor().getToolkit(), form); + setContainer(page); + } + + public FormPage getPage() { + return (FormPage)getContainer(); + } + public void dirtyStateChanged() { + getPage().getEditor().editorDirtyStateChanged(); + } + public void staleStateChanged() { + if (getPage().isActive()) + refresh(); + } + } + /** + * A constructor that creates the page and initializes it with the editor. + * + * @param editor + * the parent editor + * @param id + * the unique identifier + * @param title + * the page title + */ + public FormPage(FormEditor editor, String id, String title) { + this(id, title); + initialize(editor); + } + /** + * The constructor. The parent editor need to be passed in the + * initialize method if this constructor is used. + * + * @param id + * a unique page identifier + * @param title + * a user-friendly page title + */ + public FormPage(String id, String title) { + this.id = id; + setPartName(title); + } + /** + * Initializes the form page. + * + * @see IEditorPart#init + */ +// public void init(IEditorSite site, IEditorInput input) { +// setSite(site); +// setInput(input); +// } + /** + * Primes the form page with the parent editor instance. + * + * @param editor + * the parent editor + */ + public void initialize(FormEditor editor) { + this.editor = editor; + } + /** + * Returns the parent editor. + * + * @return parent editor instance + */ + public FormEditor getEditor() { + return editor; + } + /** + * Returns the managed form owned by this page. + * + * @return the managed form + */ + public IManagedForm getManagedForm() { + return mform; + } + /** + * Implements the required method by refreshing the form when set active. + * Subclasses must call super when overriding this method. + */ + public void setActive(boolean active) { + if (active) { + // We are switching to this page - refresh it + // if needed. + if (mform != null) + mform.refresh(); + } + } + /** + * Tests if the page is active by asking the parent editor if this page is + * the currently active page. + * + * @return true if the page is currently active, + * false otherwise. + */ + public boolean isActive() { + return this.equals(editor.getActivePageInstance()); + } + /** + * Creates the part control by creating the managed form using the parent + * editor's toolkit. Subclasses should override + * createFormContent(IManagedForm) to populate the form with + * content. + * + * @param parent + * the page parent composite + */ + public void createPartControl(Composite parent) { + ScrolledComposite form = editor.getToolkit().createScrolledForm(parent); + mform = new PageForm(this, form); + BusyIndicator.showWhile(parent.getDisplay(), new Runnable() { + public void run() { + createFormContent(mform); + } + }); + } + /** + * Subclasses should override this method to create content in the form + * hosted in this page. + * + * @param managedForm + * the form hosted in this page. + */ + protected void createFormContent(IManagedForm managedForm) { + } + /** + * Returns the form page control. + * + * @return managed form's control + */ + public Control getPartControl() { + return mform != null ? mform.getForm() : null; + } + /** + * Disposes the managed form. + */ + public void dispose() { + if (mform != null) + mform.dispose(); + } + /** + * Returns the unique identifier that can be used to reference this page. + * + * @return the unique page identifier + */ + public String getId() { + return id; + } + /** + * Returns null- form page has no title image. Subclasses + * may override. + * + * @return null + */ + public Image getTitleImage() { + return null; + } + /** + * Sets the focus by delegating to the managed form. + */ + public void setFocus() { + if (mform != null) + mform.setFocus(); + } + /** + * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor) + */ +// public void doSave(IProgressMonitor monitor) { +// if (mform != null) +// mform.commit(true); +// } + /** + * @see org.eclipse.ui.ISaveablePart#doSaveAs() + */ + public void doSaveAs() { + } + /** + * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed() + */ + public boolean isSaveAsAllowed() { + return false; + } + /** + * Implemented by testing if the managed form is dirty. + * + * @return true if the managed form is dirty, + * false otherwise. + * + * @see org.eclipse.ui.ISaveablePart#isDirty() + */ + public boolean isDirty() { + return mform != null ? mform.isDirty() : false; + } + /** + * Preserves the page index. + * + * @param index + * the assigned page index + */ + public void setIndex(int index) { + this.index = index; + } + /** + * Returns the saved page index. + * + * @return the page index + */ + public int getIndex() { + return index; + } + /** + * Form pages are not editors. + * + * @return false + */ + public boolean isEditor() { + return false; + } + /** + * Attempts to select and reveal the given object by passing the request to + * the managed form. + * + * @param object + * the object to select and reveal in the page if possible. + * @return true if the page has been successfully selected + * and revealed by one of the managed form parts, false + * otherwise. + */ + public boolean selectReveal(Object object) { + if (mform != null) + return mform.setInput(object); + return false; + } + /** + * By default, editor will be allowed to flip the page. + * @return true + */ + public boolean canLeaveThePage() { + return true; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java new file mode 100644 index 000000000..eb08cb59d --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java @@ -0,0 +1,119 @@ +package org.argeo.cms.ui.eclipse.forms.editor; +import org.argeo.cms.ui.eclipse.forms.IManagedForm; +import org.eclipse.swt.widgets.Control; +/** + * Interface that all GUI pages need to implement in order + * to be added to FormEditor part. The interface makes + * several assumptions: + *

    + *
  • The form page has a managed form
  • + *
  • The form page has a unique id
  • + *
  • The form page can be GUI but can also wrap a complete + * editor class (in that case, it should return true + * from isEditor() method).
  • + *
  • The form page is lazy i.e. understands that + * its part control will be created at the last possible + * moment.
  • . + *
+ *

Existing editors can be wrapped by implementing + * this interface. In this case, 'isEditor' should return true. + * A common editor to wrap in TextEditor that is + * often added to show the raw source code of the file open into + * the multi-page editor. + * + * @since 1.0 + */ +public interface IFormPage { + /** + * @param editor + * the form editor that this page belongs to + */ + void initialize(FormEditor editor); + /** + * Returns the editor this page belongs to. + * + * @return the form editor + */ + FormEditor getEditor(); + /** + * Returns the managed form of this page, unless this is a source page. + * + * @return the managed form or null if this is a source page. + */ + IManagedForm getManagedForm(); + /** + * Indicates whether the page has become the active in the editor. Classes + * that implement this interface may use this method to commit the page (on + * false) or lazily create and/or populate the content on + * true. + * + * @param active + * true if page should be visible, false + * otherwise. + */ + void setActive(boolean active); + /** + * Returns true if page is currently active, false if not. + * + * @return true for active page. + */ + boolean isActive(); + /** + * Tests if the content of the page is in a state that allows the + * editor to flip to another page. Typically, pages that contain + * raw source with syntax errors should not allow editors to + * leave them until errors are corrected. + * @return true if the editor can flip to another page, + * false otherwise. + */ + boolean canLeaveThePage(); + /** + * Returns the control associated with this page. + * + * @return the control of this page if created or null if the + * page has not been shown yet. + */ + Control getPartControl(); + /** + * Page must have a unique id that can be used to show it without knowing + * its relative position in the editor. + * + * @return the unique page identifier + */ + String getId(); + /** + * Returns the position of the page in the editor. + * + * @return the zero-based index of the page in the editor. + */ + int getIndex(); + /** + * Sets the position of the page in the editor. + * + * @param index + * the zero-based index of the page in the editor. + */ + void setIndex(int index); + /** + * Tests whether this page wraps a complete editor that + * can be registered on its own, or represents a page + * that cannot exist outside the multi-page editor context. + * + * @return true if the page wraps an editor, + * false if this is a form page. + */ + boolean isEditor(); + /** + * A hint to bring the provided object into focus. If the object is in a + * tree or table control, select it. If it is shown on a scrollable page, + * ensure that it is visible. If the object is not presented in + * the page, false should be returned to allow another + * page to try. + * + * @param object + * object to select and reveal + * @return true if the request was successful, false + * otherwise. + */ + boolean selectReveal(Object object); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java new file mode 100644 index 000000000..380634118 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java @@ -0,0 +1,75 @@ +package org.argeo.cms.ui.jcr; + +import java.util.Collections; +import java.util.Map; +import java.util.Observable; +import java.util.TreeMap; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; + +public class DefaultRepositoryRegister extends Observable implements RepositoryRegister { + /** Key for a JCR repository alias */ + private final static String CN = CmsConstants.CN; + /** Key for a JCR repository URI */ + // public final static String JCR_REPOSITORY_URI = "argeo.jcr.repository.uri"; + private final static CmsLog log = CmsLog.getLog(DefaultRepositoryRegister.class); + + /** Read only map which will be directly exposed. */ + private Map repositories = Collections.unmodifiableMap(new TreeMap()); + + @SuppressWarnings("rawtypes") + public synchronized Repository getRepository(Map parameters) throws RepositoryException { + if (!parameters.containsKey(CN)) + throw new RepositoryException("Parameter " + CN + " has to be defined."); + String alias = parameters.get(CN).toString(); + if (!repositories.containsKey(alias)) + throw new RepositoryException("No repository registered with alias " + alias); + + return repositories.get(alias); + } + + /** Access to the read-only map */ + public synchronized Map getRepositories() { + return repositories; + } + + /** Registers a service, typically called when OSGi services are bound. */ + @SuppressWarnings("rawtypes") + public synchronized void register(Repository repository, Map properties) { + String alias; + if (properties == null || !properties.containsKey(CN)) { + log.warn("Cannot register a repository if no " + CN + " property is specified."); + return; + } + alias = properties.get(CN).toString(); + Map map = new TreeMap(repositories); + map.put(alias, repository); + repositories = Collections.unmodifiableMap(map); + setChanged(); + notifyObservers(alias); + } + + /** Unregisters a service, typically called when OSGi services are unbound. */ + @SuppressWarnings("rawtypes") + public synchronized void unregister(Repository repository, Map properties) { + // TODO: also check bean name? + if (properties == null || !properties.containsKey(CN)) { + log.warn("Cannot unregister a repository without property " + CN); + return; + } + + String alias = properties.get(CN).toString(); + Map map = new TreeMap(repositories); + if (map.remove(alias) == null) { + log.warn("No repository was registered with alias " + alias); + return; + } + repositories = Collections.unmodifiableMap(map); + setChanged(); + notifyObservers(alias); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java new file mode 100644 index 000000000..0f7ee7735 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java @@ -0,0 +1,98 @@ +package org.argeo.cms.ui.jcr; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.argeo.eclipse.ui.EclipseUiException; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Display some version information of a JCR full versionable node in a tree + * like structure + */ +public class FullVersioningTreeContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = 8691772509491211112L; + + /** + * Sends back the first level of the Tree. input element must be a single + * node object + */ + public Object[] getElements(Object inputElement) { + try { + Node rootNode = (Node) inputElement; + String curPath = rootNode.getPath(); + VersionManager vm = rootNode.getSession().getWorkspace() + .getVersionManager(); + + VersionHistory vh = vm.getVersionHistory(curPath); + List result = new ArrayList(); + VersionIterator vi = vh.getAllLinearVersions(); + + while (vi.hasNext()) { + result.add(vi.nextVersion()); + } + return result.toArray(); + } catch (RepositoryException re) { + throw new EclipseUiException( + "Unexpected error while getting version elements", re); + } + } + + public Object[] getChildren(Object parentElement) { + try { + if (parentElement instanceof Version) { + List tmp = new ArrayList(); + tmp.add(((Version) parentElement).getFrozenNode()); + return tmp.toArray(); + } + } catch (RepositoryException re) { + throw new EclipseUiException("Unexpected error while getting child " + + "node for version element", re); + } + return null; + } + + public Object getParent(Object element) { + try { + // this will not work in a simpleVersionning environment, parent is + // not a node. + if (element instanceof Node + && ((Node) element).isNodeType(NodeType.NT_FROZEN_NODE)) { + Node node = (Node) element; + return node.getParent(); + } else + return null; + } catch (RepositoryException e) { + return null; + } + } + + public boolean hasChildren(Object element) { + try { + if (element instanceof Version) + return true; + else if (element instanceof Node) + return ((Node) element).hasNodes(); + else + return false; + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot check children of " + element, e); + } + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java new file mode 100644 index 000000000..b36acc368 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java @@ -0,0 +1,68 @@ +package org.argeo.cms.ui.jcr; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.argeo.cms.ui.jcr.model.RepositoriesElem; +import org.argeo.cms.ui.jcr.model.RepositoryElem; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ui.jcr.model.WorkspaceElem; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; + +/** Useful methods to manage the JCR Browser */ +public class JcrBrowserUtils { + + public static String getPropertyTypeAsString(Property prop) { + try { + return PropertyType.nameFromValue(prop.getType()); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot check type for " + prop, e); + } + } + + /** Insure that the UI component is not stale, refresh if needed */ + public static void forceRefreshIfNeeded(TreeParent element) { + Node curNode = null; + + boolean doRefresh = false; + + try { + if (element instanceof SingleJcrNodeElem) { + curNode = ((SingleJcrNodeElem) element).getNode(); + } else if (element instanceof WorkspaceElem) { + curNode = ((WorkspaceElem) element).getRootNode(); + } + + if (curNode != null && element.getChildren().length != curNode.getNodes().getSize()) + doRefresh = true; + else if (element instanceof RepositoryElem) { + RepositoryElem rn = (RepositoryElem) element; + if (rn.isConnected()) { + String[] wkpNames = rn.getAccessibleWorkspaceNames(); + if (element.getChildren().length != wkpNames.length) + doRefresh = true; + } + } else if (element instanceof RepositoriesElem) { + doRefresh = true; + // Always force refresh for RepositoriesElem : the condition + // below does not take remote repository into account and it is + // not trivial to do so. + + // RepositoriesElem rn = (RepositoriesElem) element; + // if (element.getChildren().length != + // rn.getRepositoryRegister() + // .getRepositories().size()) + // doRefresh = true; + } + if (doRefresh) { + element.clearChildren(); + element.getChildren(); + } + } catch (RepositoryException re) { + throw new EclipseUiException("Unexpected error while synchronising the UI with the JCR repository", re); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrDClickListener.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrDClickListener.java new file mode 100644 index 000000000..1707681b4 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrDClickListener.java @@ -0,0 +1,60 @@ +package org.argeo.cms.ui.jcr; + +import javax.jcr.Node; + +import org.argeo.cms.ui.jcr.model.RepositoryElem; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ui.jcr.model.WorkspaceElem; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TreeViewer; + +/** Centralizes the management of double click on a NodeTreeViewer */ +public class JcrDClickListener implements IDoubleClickListener { + // private final static Log log = LogFactory + // .getLog(GenericNodeDoubleClickListener.class); + + private TreeViewer nodeViewer; + + // private JcrFileProvider jfp; + // private FileHandler fileHandler; + + public JcrDClickListener(TreeViewer nodeViewer) { + this.nodeViewer = nodeViewer; + // jfp = new JcrFileProvider(); + // Commented out. see https://www.argeo.org/bugzilla/show_bug.cgi?id=188 + // fileHandler = null; + // fileHandler = new FileHandler(jfp); + } + + public void doubleClick(DoubleClickEvent event) { + if (event.getSelection() == null || event.getSelection().isEmpty()) + return; + Object obj = ((IStructuredSelection) event.getSelection()).getFirstElement(); + if (obj instanceof RepositoryElem) { + RepositoryElem rpNode = (RepositoryElem) obj; + if (rpNode.isConnected()) { + rpNode.logout(); + } else { + rpNode.login(); + } + nodeViewer.refresh(obj); + } else if (obj instanceof WorkspaceElem) { + WorkspaceElem wn = (WorkspaceElem) obj; + if (wn.isConnected()) + wn.logout(); + else + wn.login(); + nodeViewer.refresh(obj); + } else if (obj instanceof SingleJcrNodeElem) { + SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj; + Node node = sjn.getNode(); + openNode(node); + } + } + + protected void openNode(Node node) { + // TODO implement generic behaviour + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrImages.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrImages.java new file mode 100644 index 000000000..d1d1e31ef --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrImages.java @@ -0,0 +1,24 @@ +package org.argeo.cms.ui.jcr; + +import org.argeo.cms.ui.theme.CmsImages; +import org.eclipse.swt.graphics.Image; + +/** Shared icons. */ +public class JcrImages { + public final static Image NODE = CmsImages.createIcon("node.gif"); + public final static Image FOLDER = CmsImages.createIcon("folder.gif"); + public final static Image FILE = CmsImages.createIcon("file.gif"); + public final static Image BINARY = CmsImages.createIcon("binary.png"); + public final static Image HOME = CmsImages.createIcon("person-logged-in.png"); + public final static Image SORT = CmsImages.createIcon("sort.gif"); + public final static Image REMOVE = CmsImages.createIcon("remove.gif"); + + public final static Image REPOSITORIES = CmsImages.createIcon("repositories.gif"); + public final static Image REPOSITORY_DISCONNECTED = CmsImages.createIcon("repository_disconnected.gif"); + public final static Image REPOSITORY_CONNECTED = CmsImages.createIcon("repository_connected.gif"); + public final static Image REMOTE_DISCONNECTED = CmsImages.createIcon("remote_disconnected.gif"); + public final static Image REMOTE_CONNECTED = CmsImages.createIcon("remote_connected.gif"); + public final static Image WORKSPACE_DISCONNECTED = CmsImages.createIcon("workspace_disconnected.png"); + public final static Image WORKSPACE_CONNECTED = CmsImages.createIcon("workspace_connected.png"); + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java new file mode 100644 index 000000000..cc8479f78 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java @@ -0,0 +1,82 @@ +package org.argeo.cms.ui.jcr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; + +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.jcr.util.JcrItemsComparator; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Implementation of the {@code ITreeContentProvider} in order to display a + * single JCR node and its children in a tree like structure + */ +public class JcrTreeContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = -2128326504754297297L; + // private Node rootNode; + private JcrItemsComparator itemComparator = new JcrItemsComparator(); + + /** + * Sends back the first level of the Tree. input element must be a single node + * object + */ + public Object[] getElements(Object inputElement) { + Node rootNode = (Node) inputElement; + return childrenNodes(rootNode); + } + + public Object[] getChildren(Object parentElement) { + return childrenNodes((Node) parentElement); + } + + public Object getParent(Object element) { + try { + Node node = (Node) element; + if (!node.getPath().equals("/")) + return node.getParent(); + else + return null; + } catch (RepositoryException e) { + return null; + } + } + + public boolean hasChildren(Object element) { + try { + return ((Node) element).hasNodes(); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot check children existence on " + element, e); + } + } + + protected Object[] childrenNodes(Node parentNode) { + try { + List children = new ArrayList(); + NodeIterator nit = parentNode.getNodes(); + while (nit.hasNext()) { + Node node = nit.nextNode(); +// if (node.getName().startsWith("rep:") || node.getName().startsWith("jcr:") +// || node.getName().startsWith("nt:")) +// continue nodes; + children.add(node); + } + Node[] arr = children.toArray(new Node[0]); + Arrays.sort(arr, itemComparator); + return arr; + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot list children of " + parentNode, e); + } + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeContentProvider.java new file mode 100644 index 000000000..0625cc872 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeContentProvider.java @@ -0,0 +1,175 @@ +package org.argeo.cms.ui.jcr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.ui.jcr.model.RepositoriesElem; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ux.widgets.TreeParent; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Implementation of the {@code ITreeContentProvider} to display multiple + * repository environment in a tree like structure + */ +public class NodeContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = -4083809398848374403L; + final private RepositoryRegister repositoryRegister; + final private RepositoryFactory repositoryFactory; + + // Current user session on the default workspace of the argeo Node + final private Session userSession; + final private Keyring keyring; + private boolean sortChildren; + + // Reference for cleaning + private SingleJcrNodeElem homeNode = null; + private RepositoriesElem repositoriesNode = null; + + // Utils + private TreeBrowserComparator itemComparator = new TreeBrowserComparator(); + + public NodeContentProvider(Session userSession, Keyring keyring, + RepositoryRegister repositoryRegister, + RepositoryFactory repositoryFactory, Boolean sortChildren) { + this.userSession = userSession; + this.keyring = keyring; + this.repositoryRegister = repositoryRegister; + this.repositoryFactory = repositoryFactory; + this.sortChildren = sortChildren; + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (newInput == null)// dispose + return; + + if (userSession != null) { + Node userHome = CmsJcrUtils.getUserHome(userSession); + if (userHome != null) { + // TODO : find a way to dynamically get alias for the node + if (homeNode != null) + homeNode.dispose(); + homeNode = new SingleJcrNodeElem(null, userHome, + userSession.getUserID(), CmsConstants.EGO_REPOSITORY); + } + } + if (repositoryRegister != null) { + if (repositoriesNode != null) + repositoriesNode.dispose(); + repositoriesNode = new RepositoriesElem("Repositories", + repositoryRegister, repositoryFactory, null, userSession, + keyring); + } + } + + /** + * Sends back the first level of the Tree. Independent from inputElement + * that can be null + */ + public Object[] getElements(Object inputElement) { + List objs = new ArrayList(); + if (homeNode != null) + objs.add(homeNode); + if (repositoriesNode != null) + objs.add(repositoriesNode); + return objs.toArray(); + } + + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof TreeParent) { + if (sortChildren) { + Object[] tmpArr = ((TreeParent) parentElement).getChildren(); + if (tmpArr == null) + return new Object[0]; + TreeParent[] arr = new TreeParent[tmpArr.length]; + for (int i = 0; i < tmpArr.length; i++) + arr[i] = (TreeParent) tmpArr[i]; + Arrays.sort(arr, itemComparator); + return arr; + } else + return ((TreeParent) parentElement).getChildren(); + } else + return new Object[0]; + } + + /** + * Sets whether the content provider should order the children nodes or not. + * It is user duty to call a full refresh of the tree after changing this + * parameter. + */ + public void setSortChildren(boolean sortChildren) { + this.sortChildren = sortChildren; + } + + public Object getParent(Object element) { + if (element instanceof TreeParent) { + return ((TreeParent) element).getParent(); + } else + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof RepositoriesElem) { + RepositoryRegister rr = ((RepositoriesElem) element) + .getRepositoryRegister(); + return rr.getRepositories().size() > 0; + } else if (element instanceof TreeParent) { + TreeParent tp = (TreeParent) element; + return tp.hasChildren(); + } + return false; + } + + public void dispose() { + if (homeNode != null) + homeNode.dispose(); + if (repositoriesNode != null) { + // logs out open sessions + // see https://bugzilla.argeo.org/show_bug.cgi?id=23 + repositoriesNode.dispose(); + } + } + + /** + * Specific comparator for this view. See specification here: + * https://www.argeo.org/bugzilla/show_bug.cgi?id=139 + */ + private class TreeBrowserComparator implements Comparator { + + public int category(TreeParent element) { + if (element instanceof SingleJcrNodeElem) { + Node node = ((SingleJcrNodeElem) element).getNode(); + try { + if (node.isNodeType(NodeType.NT_FOLDER)) + return 5; + } catch (RepositoryException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + return 10; + } + + public int compare(TreeParent o1, TreeParent o2) { + int cat1 = category(o1); + int cat2 = category(o2); + + if (cat1 != cat2) { + return cat1 - cat2; + } + return o1.getName().compareTo(o2.getName()); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java new file mode 100644 index 000000000..a5751c083 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java @@ -0,0 +1,113 @@ +package org.argeo.cms.ui.jcr; + +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.ui.jcr.model.RemoteRepositoryElem; +import org.argeo.cms.ui.jcr.model.RepositoriesElem; +import org.argeo.cms.ui.jcr.model.RepositoryElem; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ui.jcr.model.WorkspaceElem; +import org.argeo.eclipse.ui.EclipseUiException; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Image; + +/** Provides reasonable defaults for know JCR types. */ +public class NodeLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -3662051696443321843L; + + private final static CmsLog log = CmsLog.getLog(NodeLabelProvider.class); + + public String getText(Object element) { + try { + if (element instanceof SingleJcrNodeElem) { + SingleJcrNodeElem sjn = (SingleJcrNodeElem) element; + return getText(sjn.getNode()); + } else if (element instanceof Node) { + return getText((Node) element); + } else + return super.getText(element); + } catch (RepositoryException e) { + throw new EclipseUiException("Unexpected JCR error while getting node name."); + } + } + + protected String getText(Node node) throws RepositoryException { + String label = node.getName(); + StringBuffer mixins = new StringBuffer(""); + for (NodeType type : node.getMixinNodeTypes()) + mixins.append(' ').append(type.getName()); + + return label + " [" + node.getPrimaryNodeType().getName() + mixins + "]"; + } + + @Override + public Image getImage(Object element) { + if (element instanceof RemoteRepositoryElem) { + if (((RemoteRepositoryElem) element).isConnected()) + return JcrImages.REMOTE_CONNECTED; + else + return JcrImages.REMOTE_DISCONNECTED; + } else if (element instanceof RepositoryElem) { + if (((RepositoryElem) element).isConnected()) + return JcrImages.REPOSITORY_CONNECTED; + else + return JcrImages.REPOSITORY_DISCONNECTED; + } else if (element instanceof WorkspaceElem) { + if (((WorkspaceElem) element).isConnected()) + return JcrImages.WORKSPACE_CONNECTED; + else + return JcrImages.WORKSPACE_DISCONNECTED; + } else if (element instanceof RepositoriesElem) { + return JcrImages.REPOSITORIES; + } else if (element instanceof SingleJcrNodeElem) { + Node nodeElem = ((SingleJcrNodeElem) element).getNode(); + return getImage(nodeElem); + + // if (element instanceof Node) { + // return getImage((Node) element); + // } else if (element instanceof WrappedNode) { + // return getImage(((WrappedNode) element).getNode()); + // } else if (element instanceof NodesWrapper) { + // return getImage(((NodesWrapper) element).getNode()); + // } + } + // try { + // return super.getImage(); + // } catch (RepositoryException e) { + // return null; + // } + return super.getImage(element); + } + + protected Image getImage(Node node) { + try { + if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)) + return JcrImages.FILE; + else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) + return JcrImages.FOLDER; + else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_RESOURCE)) + return JcrImages.BINARY; + try { + // TODO check workspace type? + if (node.getDepth() == 1 && node.hasProperty(Property.JCR_ID)) + return JcrImages.HOME; + + // optimizes +// if (node.hasProperty(LdapAttrs.uid.property()) && node.isNodeType(NodeTypes.NODE_USER_HOME)) +// return JcrImages.HOME; + } catch (NamespaceException e) { + // node namespace is not registered in this repo + } + return JcrImages.NODE; + } catch (RepositoryException e) { + log.warn("Error while retrieving type for " + node + " in order to display corresponding image"); + e.printStackTrace(); + return null; + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java new file mode 100644 index 000000000..444350aeb --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java @@ -0,0 +1,52 @@ +package org.argeo.cms.ui.jcr; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +public class OsgiRepositoryRegister extends DefaultRepositoryRegister { + private final static BundleContext bc = FrameworkUtil.getBundle(OsgiRepositoryRegister.class).getBundleContext(); + private final ServiceTracker repositoryTracker; + + public OsgiRepositoryRegister() { + repositoryTracker = new ServiceTracker(bc, Repository.class, null) { + + @Override + public Repository addingService(ServiceReference reference) { + + Repository repository = super.addingService(reference); + Map props = new HashMap<>(); + for (String key : reference.getPropertyKeys()) { + props.put(key, reference.getProperty(key)); + } + register(repository, props); + return repository; + } + + @Override + public void removedService(ServiceReference reference, Repository service) { + Map props = new HashMap<>(); + for (String key : reference.getPropertyKeys()) { + props.put(key, reference.getProperty(key)); + } + unregister(service, props); + super.removedService(reference, service); + } + + }; + } + + public void init() { + repositoryTracker.open(); + } + + public void destroy() { + repositoryTracker.close(); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java new file mode 100644 index 000000000..fd544bbd8 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java @@ -0,0 +1,42 @@ +package org.argeo.cms.ui.jcr; + +import java.util.Set; +import java.util.TreeSet; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.jcr.util.JcrItemsComparator; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** Simple content provider that displays all properties of a given Node */ +public class PropertiesContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = 5227554668841613078L; + private JcrItemsComparator itemComparator = new JcrItemsComparator(); + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + public Object[] getElements(Object inputElement) { + try { + if (inputElement instanceof Node) { + Set props = new TreeSet(itemComparator); + PropertyIterator pit = ((Node) inputElement).getProperties(); + while (pit.hasNext()) + props.add(pit.nextProperty()); + return props.toArray(); + } + return new Object[] {}; + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot get element for " + + inputElement, e); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java new file mode 100644 index 000000000..58d60317a --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java @@ -0,0 +1,102 @@ +package org.argeo.cms.ui.jcr; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.jcr.JcrUtils; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ViewerCell; + +/** Default basic label provider for a given JCR Node's properties */ +public class PropertyLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -5405794508731390147L; + + // To be able to change column order easily + public static final int COLUMN_PROPERTY = 0; + public static final int COLUMN_VALUE = 1; + public static final int COLUMN_TYPE = 2; + public static final int COLUMN_ATTRIBUTES = 3; + + private final static String DATE_TIME_FORMAT = "dd/MM/yyyy, HH:mm"; + + // Utils + protected DateFormat timeFormatter = new SimpleDateFormat(DATE_TIME_FORMAT); + + public void update(ViewerCell cell) { + Object element = cell.getElement(); + cell.setText(getColumnText(element, cell.getColumnIndex())); + } + + public String getColumnText(Object element, int columnIndex) { + try { + if (element instanceof Property) { + Property prop = (Property) element; + if (prop.isMultiple()) { + switch (columnIndex) { + case COLUMN_PROPERTY: + return prop.getName(); + case COLUMN_VALUE: + // Corresponding values are listed on children + return ""; + case COLUMN_TYPE: + return JcrBrowserUtils.getPropertyTypeAsString(prop); + case COLUMN_ATTRIBUTES: + return JcrUtils.getPropertyDefinitionAsString(prop); + } + } else { + switch (columnIndex) { + case COLUMN_PROPERTY: + return prop.getName(); + case COLUMN_VALUE: + return formatValueAsString(prop.getValue()); + case COLUMN_TYPE: + return JcrBrowserUtils.getPropertyTypeAsString(prop); + case COLUMN_ATTRIBUTES: + return JcrUtils.getPropertyDefinitionAsString(prop); + } + } + } else if (element instanceof Value) { + Value val = (Value) element; + switch (columnIndex) { + case COLUMN_PROPERTY: + // Nothing to show + return ""; + case COLUMN_VALUE: + return formatValueAsString(val); + case COLUMN_TYPE: + // listed on the parent + return ""; + case COLUMN_ATTRIBUTES: + // Corresponding attributes are listed on the parent + return ""; + } + } + } catch (RepositoryException re) { + throw new EclipseUiException("Cannot retrieve prop value on " + element, re); + } + return null; + } + + private String formatValueAsString(Value value) { + // TODO enhance this method + try { + String strValue; + + if (value.getType() == PropertyType.BINARY) + strValue = ""; + else if (value.getType() == PropertyType.DATE) + strValue = timeFormatter.format(value.getDate().getTime()); + else + strValue = value.getString(); + return strValue; + } catch (RepositoryException e) { + throw new EclipseUiException("unexpected error while formatting value", e); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/RepositoryRegister.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/RepositoryRegister.java new file mode 100644 index 000000000..802c75619 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/RepositoryRegister.java @@ -0,0 +1,16 @@ +package org.argeo.cms.ui.jcr; + +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryFactory; + +/** Allows to register repositories by name. */ +public interface RepositoryRegister extends RepositoryFactory { + /** + * The registered {@link Repository} as a read-only map. Note that this + * method should be called for each access in order to be sure to be up to + * date in case repositories have registered/unregistered + */ + public Map getRepositories(); +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java new file mode 100644 index 000000000..37dfe2b8f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java @@ -0,0 +1,33 @@ +package org.argeo.cms.ui.jcr; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; + +import org.argeo.eclipse.ui.EclipseUiException; +import org.eclipse.jface.viewers.ColumnLabelProvider; + +/** + * Simple wrapping of the ColumnLabelProvider class to provide text display in + * order to build a tree for version. The getText() method does not assume that + * {@link Version} extends {@link Node} class to respect JCR 2.0 specification + * + */ +public class VersionLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = 5270739851193688238L; + + public String getText(Object element) { + try { + if (element instanceof Version) { + Version version = (Version) element; + return version.getName(); + } else if (element instanceof Node) { + return ((Node) element).getName(); + } + } catch (RepositoryException re) { + throw new EclipseUiException( + "Unexpected error while getting element name", re); + } + return super.getText(element); + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java new file mode 100644 index 000000000..d33b33f63 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java @@ -0,0 +1,21 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.Repository; + +import org.argeo.cms.ux.widgets.TreeParent; + +/** Wrap a MaintainedRepository */ +public class MaintainedRepositoryElem extends RepositoryElem { + + public MaintainedRepositoryElem(String alias, Repository repository, TreeParent parent) { + super(alias, repository, parent); + // if (!(repository instanceof MaintainedRepository)) { + // throw new ArgeoException("Repository " + alias + // + " is not a maintained repository"); + // } + } + + // protected MaintainedRepository getMaintainedRepository() { + // return (MaintainedRepository) getRepository(); + // } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java new file mode 100644 index 000000000..908d1b135 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java @@ -0,0 +1,76 @@ +package org.argeo.cms.ui.jcr.model; + +import java.util.Arrays; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.argeo.cms.ArgeoNames; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; + +/** Root of a remote repository */ +public class RemoteRepositoryElem extends RepositoryElem { + private final Keyring keyring; + /** + * A session of the logged in user on the default workspace of the node + * repository. + */ + private final Session userSession; + private final String remoteNodePath; + + private final RepositoryFactory repositoryFactory; + private final String uri; + + public RemoteRepositoryElem(String alias, RepositoryFactory repositoryFactory, String uri, TreeParent parent, + Session userSession, Keyring keyring, String remoteNodePath) { + super(alias, null, parent); + this.repositoryFactory = repositoryFactory; + this.uri = uri; + this.keyring = keyring; + this.userSession = userSession; + this.remoteNodePath = remoteNodePath; + } + + @Override + protected Session repositoryLogin(String workspaceName) throws RepositoryException { + Node remoteRepository = userSession.getNode(remoteNodePath); + String userID = remoteRepository.getProperty(ArgeoNames.ARGEO_USER_ID).getString(); + if (userID.trim().equals("")) { + return getRepository().login(workspaceName); + } else { + String pwdPath = remoteRepository.getPath() + '/' + ArgeoNames.ARGEO_PASSWORD; + char[] password = keyring.getAsChars(pwdPath); + try { + SimpleCredentials credentials = new SimpleCredentials(userID, password); + return getRepository().login(credentials, workspaceName); + } finally { + Arrays.fill(password, 0, password.length, ' '); + } + } + } + + @Override + public Repository getRepository() { + if (repository == null) + repository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, uri); + return super.getRepository(); + } + + public void remove() { + try { + Node remoteNode = userSession.getNode(remoteNodePath); + remoteNode.remove(); + remoteNode.getSession().save(); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot remove " + remoteNodePath, e); + } + } + +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java new file mode 100644 index 000000000..8c40f8b37 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java @@ -0,0 +1,112 @@ +package org.argeo.cms.ui.jcr.model; + +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; + +import org.argeo.cms.ArgeoNames; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.ui.jcr.RepositoryRegister; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; + +/** + * UI Tree component that implements the Argeo abstraction of a + * {@link RepositoryFactory} that enable a user to "mount" various repositories + * in a single Tree like View. It is usually meant to be at the root of the UI + * Tree and thus {@link #getParent()} method will return null. + * + * The {@link RepositoryFactory} is injected at instantiation time and must be + * use get or register new {@link Repository} objects upon which a reference is + * kept here. + */ + +public class RepositoriesElem extends TreeParent implements ArgeoNames { + private final RepositoryRegister repositoryRegister; + private final RepositoryFactory repositoryFactory; + + /** + * A session of the logged in user on the default workspace of the node + * repository. + */ + private final Session userSession; + private final Keyring keyring; + + public RepositoriesElem(String name, RepositoryRegister repositoryRegister, RepositoryFactory repositoryFactory, + TreeParent parent, Session userSession, Keyring keyring) { + super(name); + this.repositoryRegister = repositoryRegister; + this.repositoryFactory = repositoryFactory; + this.userSession = userSession; + this.keyring = keyring; + } + + /** + * Override normal behavior to initialize the various repositories only at + * request time + */ + @Override + public synchronized Object[] getChildren() { + if (isLoaded()) { + return super.getChildren(); + } else { + // initialize current object + Map refRepos = repositoryRegister.getRepositories(); + for (String name : refRepos.keySet()) { + Repository repository = refRepos.get(name); + // if (repository instanceof MaintainedRepository) + // super.addChild(new MaintainedRepositoryElem(name, + // repository, this)); + // else + super.addChild(new RepositoryElem(name, repository, this)); + } + + // remote + if (keyring != null) { + try { + addRemoteRepositories(keyring); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot browse remote repositories", e); + } + } + return super.getChildren(); + } + } + + protected void addRemoteRepositories(Keyring jcrKeyring) throws RepositoryException { + Node userHome = CmsJcrUtils.getUserHome(userSession); + if (userHome != null && userHome.hasNode(ARGEO_REMOTE)) { + NodeIterator it = userHome.getNode(ARGEO_REMOTE).getNodes(); + while (it.hasNext()) { + Node remoteNode = it.nextNode(); + String uri = remoteNode.getProperty(ARGEO_URI).getString(); + try { + RemoteRepositoryElem remoteRepositoryNode = new RemoteRepositoryElem(remoteNode.getName(), + repositoryFactory, uri, this, userSession, jcrKeyring, remoteNode.getPath()); + super.addChild(remoteRepositoryNode); + } catch (Exception e) { + ErrorFeedback.show("Cannot add remote repository " + remoteNode, e); + } + } + } + } + + public void registerNewRepository(String alias, Repository repository) { + // TODO: implement this + // Create a new RepositoryNode Object + // add it + // super.addChild(new RepositoriesNode(...)); + } + + /** Returns the {@link RepositoryRegister} wrapped by this object. */ + public RepositoryRegister getRepositoryRegister() { + return repositoryRegister; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java new file mode 100644 index 000000000..296c36922 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java @@ -0,0 +1,98 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.jcr.JcrUtils; + +/** + * UI Tree component that wraps a JCR {@link Repository}. It also keeps a + * reference to its parent Tree Ui component; typically the unique + * {@link RepositoriesElem} object of the current view to enable bi-directionnal + * browsing in the tree. + */ + +public class RepositoryElem extends TreeParent { + private String alias; + protected Repository repository; + private Session defaultSession = null; + + /** Create a new repository with distinct name and alias */ + public RepositoryElem(String alias, Repository repository, TreeParent parent) { + super(alias); + this.repository = repository; + setParent(parent); + this.alias = alias; + } + + public void login() { + try { + defaultSession = repositoryLogin(CmsConstants.SYS_WORKSPACE); + String[] wkpNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames(); + for (String wkpName : wkpNames) { + if (wkpName.equals(defaultSession.getWorkspace().getName())) + addChild(new WorkspaceElem(this, wkpName, defaultSession)); + else + addChild(new WorkspaceElem(this, wkpName)); + } + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot connect to repository " + alias, e); + } + } + + public synchronized void logout() { + for (Object child : getChildren()) { + if (child instanceof WorkspaceElem) + ((WorkspaceElem) child).logout(); + } + clearChildren(); + JcrUtils.logoutQuietly(defaultSession); + defaultSession = null; + } + + /** + * Actual call to the {@link Repository#login(javax.jcr.Credentials, String)} + * method. To be overridden. + */ + protected Session repositoryLogin(String workspaceName) throws RepositoryException { + return repository.login(workspaceName); + } + + public String[] getAccessibleWorkspaceNames() { + try { + return defaultSession.getWorkspace().getAccessibleWorkspaceNames(); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot retrieve workspace names", e); + } + } + + public void createWorkspace(String workspaceName) { + if (!isConnected()) + login(); + try { + defaultSession.getWorkspace().createWorkspace(workspaceName); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot create workspace", e); + } + } + + /** returns the {@link Repository} referenced by the current UI Node */ + public Repository getRepository() { + return repository; + } + + public String getAlias() { + return alias; + } + + public Boolean isConnected() { + if (defaultSession != null && defaultSession.isLive()) + return true; + else + return false; + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java new file mode 100644 index 000000000..a2584a5e8 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java @@ -0,0 +1,84 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; + +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; + +/** + * UI Tree component. Wraps a node of a JCR {@link Workspace}. It also keeps a + * reference to its parent node that can either be a {@link WorkspaceElem}, a + * {@link SingleJcrNodeElem} or null if the node is "mounted" as the root of the + * UI tree. + */ +public class SingleJcrNodeElem extends TreeParent { + + private final Node node; + private String alias = null; + + /** Creates a new UiNode in the UI Tree */ + public SingleJcrNodeElem(TreeParent parent, Node node, String name) { + super(name); + setParent(parent); + this.node = node; + } + + /** + * Creates a new UiNode in the UI Tree, keeping a reference to the alias of + * the corresponding repository in the current UI environment. It is useful + * to be able to mount nodes as roots of the UI tree. + */ + public SingleJcrNodeElem(TreeParent parent, Node node, String name, String alias) { + super(name); + setParent(parent); + this.node = node; + this.alias = alias; + } + + /** Returns the node wrapped by the current UI object */ + public Node getNode() { + return node; + } + + protected String getRepositoryAlias() { + return alias; + } + + /** + * Overrides normal behaviour to initialise children only when first + * requested + */ + @Override + public synchronized Object[] getChildren() { + if (isLoaded()) { + return super.getChildren(); + } else { + // initialize current object + try { + NodeIterator ni = node.getNodes(); + while (ni.hasNext()) { + Node curNode = ni.nextNode(); + addChild(new SingleJcrNodeElem(this, curNode, curNode.getName())); + } + return super.getChildren(); + } catch (RepositoryException re) { + throw new EclipseUiException("Cannot initialize SingleJcrNode children", re); + } + } + } + + @Override + public boolean hasChildren() { + try { + if (node.getSession().isLive()) + return node.hasNodes(); + else + return false; + } catch (RepositoryException re) { + throw new EclipseUiException("Cannot check children node existence", re); + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java new file mode 100644 index 000000000..2d786669f --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java @@ -0,0 +1,117 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +// import javax.jcr.Workspace; +import javax.jcr.Workspace; + +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.jcr.JcrUtils; + +/** + * UI Tree component. Wraps the root node of a JCR {@link Workspace}. It also + * keeps a reference to its parent {@link RepositoryElem}, to be able to + * retrieve alias of the current used repository + */ +public class WorkspaceElem extends TreeParent { + private Session session = null; + + public WorkspaceElem(RepositoryElem parent, String name) { + this(parent, name, null); + } + + public WorkspaceElem(RepositoryElem parent, String name, Session session) { + super(name); + this.session = session; + setParent(parent); + } + + public synchronized Session getSession() { + return session; + } + + public synchronized Node getRootNode() { + try { + if (session != null) + return session.getRootNode(); + else + return null; + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot get root node of workspace " + getName(), e); + } + } + + public synchronized void login() { + try { + session = ((RepositoryElem) getParent()).repositoryLogin(getName()); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot connect to repository " + getName(), e); + } + } + + public Boolean isConnected() { + if (session != null && session.isLive()) + return true; + else + return false; + } + + @Override + public synchronized void dispose() { + logout(); + super.dispose(); + } + + /** Logouts the session, does not nothing if there is no live session. */ + public synchronized void logout() { + clearChildren(); + JcrUtils.logoutQuietly(session); + session = null; + } + + @Override + public synchronized boolean hasChildren() { + try { + if (isConnected()) + try { + return session.getRootNode().hasNodes(); + } catch (AccessDeniedException e) { + // current user may not have access to the root node + return false; + } + else + return false; + } catch (RepositoryException re) { + throw new EclipseUiException("Unexpected error while checking children node existence", re); + } + } + + /** Override normal behaviour to initialize display of the workspace */ + @Override + public synchronized Object[] getChildren() { + if (isLoaded()) { + return super.getChildren(); + } else { + // initialize current object + try { + Node rootNode; + if (session == null) + return null; + else + rootNode = session.getRootNode(); + NodeIterator ni = rootNode.getNodes(); + while (ni.hasNext()) { + Node node = ni.nextNode(); + addChild(new SingleJcrNodeElem(this, node, node.getName())); + } + return super.getChildren(); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot initialize WorkspaceNode UI object." + getName(), e); + } + } + } +} diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/package-info.java new file mode 100644 index 000000000..8f5474449 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/model/package-info.java @@ -0,0 +1,2 @@ +/** Model for SWT/JFace JCR components. */ +package org.argeo.cms.ui.jcr.model; \ No newline at end of file diff --git a/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/package-info.java b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/package-info.java new file mode 100644 index 000000000..26ae330b5 --- /dev/null +++ b/swt/org.argeo.tool.devops.e4/src/org/argeo/cms/ui/jcr/package-info.java @@ -0,0 +1,2 @@ +/** SWT/JFace JCR components. */ +package org.argeo.cms.ui.jcr; \ No newline at end of file diff --git a/swt/org.argeo.tool.swt/.classpath b/swt/org.argeo.tool.swt/.classpath new file mode 100644 index 000000000..81fe078c2 --- /dev/null +++ b/swt/org.argeo.tool.swt/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/swt/org.argeo.tool.swt/.project b/swt/org.argeo.tool.swt/.project new file mode 100644 index 000000000..0ffd5462d --- /dev/null +++ b/swt/org.argeo.tool.swt/.project @@ -0,0 +1,28 @@ + + + org.argeo.tool.swt + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/swt/org.argeo.tool.swt/bnd.bnd b/swt/org.argeo.tool.swt/bnd.bnd new file mode 100644 index 000000000..2b2a02fac --- /dev/null +++ b/swt/org.argeo.tool.swt/bnd.bnd @@ -0,0 +1 @@ +Bundle-ActivationPolicy: lazy diff --git a/swt/org.argeo.tool.swt/build.properties b/swt/org.argeo.tool.swt/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/swt/org.argeo.tool.swt/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/swt/org.argeo.tool.swt/icons/actions/add.png b/swt/org.argeo.tool.swt/icons/actions/add.png new file mode 100644 index 000000000..5c06bf082 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/add.png differ diff --git a/swt/org.argeo.tool.swt/icons/actions/close-all.png b/swt/org.argeo.tool.swt/icons/actions/close-all.png new file mode 100644 index 000000000..81bfc950b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/close-all.png differ diff --git a/swt/org.argeo.tool.swt/icons/actions/delete.png b/swt/org.argeo.tool.swt/icons/actions/delete.png new file mode 100644 index 000000000..9712723d7 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/delete.png differ diff --git a/swt/org.argeo.tool.swt/icons/actions/edit.png b/swt/org.argeo.tool.swt/icons/actions/edit.png new file mode 100644 index 000000000..ad3db9f42 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/edit.png differ diff --git a/swt/org.argeo.tool.swt/icons/actions/save-all.png b/swt/org.argeo.tool.swt/icons/actions/save-all.png new file mode 100644 index 000000000..f48ed320b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/save-all.png differ diff --git a/swt/org.argeo.tool.swt/icons/actions/save.png b/swt/org.argeo.tool.swt/icons/actions/save.png new file mode 100644 index 000000000..1c58ada49 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/actions/save.png differ diff --git a/swt/org.argeo.tool.swt/icons/active.gif b/swt/org.argeo.tool.swt/icons/active.gif new file mode 100644 index 000000000..7d24707ee Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/active.gif differ diff --git a/swt/org.argeo.tool.swt/icons/add.gif b/swt/org.argeo.tool.swt/icons/add.gif new file mode 100644 index 000000000..252d7ebcb Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/add.gif differ diff --git a/swt/org.argeo.tool.swt/icons/add.png b/swt/org.argeo.tool.swt/icons/add.png new file mode 100644 index 000000000..c7edfecaa Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/add.png differ diff --git a/swt/org.argeo.tool.swt/icons/addFolder.gif b/swt/org.argeo.tool.swt/icons/addFolder.gif new file mode 100644 index 000000000..d3f43d977 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addFolder.gif differ diff --git a/swt/org.argeo.tool.swt/icons/addPrivileges.gif b/swt/org.argeo.tool.swt/icons/addPrivileges.gif new file mode 100644 index 000000000..a6b251fc8 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addPrivileges.gif differ diff --git a/swt/org.argeo.tool.swt/icons/addRepo.gif b/swt/org.argeo.tool.swt/icons/addRepo.gif new file mode 100644 index 000000000..26d81c065 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addRepo.gif differ diff --git a/swt/org.argeo.tool.swt/icons/addWorkspace.png b/swt/org.argeo.tool.swt/icons/addWorkspace.png new file mode 100644 index 000000000..bbee7755f Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/addWorkspace.png differ diff --git a/swt/org.argeo.tool.swt/icons/adminLog.gif b/swt/org.argeo.tool.swt/icons/adminLog.gif new file mode 100644 index 000000000..6ef3bca66 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/adminLog.gif differ diff --git a/swt/org.argeo.tool.swt/icons/batch.gif b/swt/org.argeo.tool.swt/icons/batch.gif new file mode 100644 index 000000000..b8ca14a8b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/batch.gif differ diff --git a/swt/org.argeo.tool.swt/icons/begin.gif b/swt/org.argeo.tool.swt/icons/begin.gif new file mode 100755 index 000000000..feb8e94a7 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/begin.gif differ diff --git a/swt/org.argeo.tool.swt/icons/binary.png b/swt/org.argeo.tool.swt/icons/binary.png new file mode 100644 index 000000000..fdf4f82be Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/binary.png differ diff --git a/swt/org.argeo.tool.swt/icons/browser.gif b/swt/org.argeo.tool.swt/icons/browser.gif new file mode 100644 index 000000000..6c7320c69 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/browser.gif differ diff --git a/swt/org.argeo.tool.swt/icons/bundles.gif b/swt/org.argeo.tool.swt/icons/bundles.gif new file mode 100644 index 000000000..e9a6bd966 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/bundles.gif differ diff --git a/swt/org.argeo.tool.swt/icons/changePassword.gif b/swt/org.argeo.tool.swt/icons/changePassword.gif new file mode 100644 index 000000000..274a850e4 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/changePassword.gif differ diff --git a/swt/org.argeo.tool.swt/icons/clear.gif b/swt/org.argeo.tool.swt/icons/clear.gif new file mode 100644 index 000000000..6bc10f9d0 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/clear.gif differ diff --git a/swt/org.argeo.tool.swt/icons/close-all.png b/swt/org.argeo.tool.swt/icons/close-all.png new file mode 100644 index 000000000..85d4d429b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/close-all.png differ diff --git a/swt/org.argeo.tool.swt/icons/commit.gif b/swt/org.argeo.tool.swt/icons/commit.gif new file mode 100755 index 000000000..876f3eb16 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/commit.gif differ diff --git a/swt/org.argeo.tool.swt/icons/delete.png b/swt/org.argeo.tool.swt/icons/delete.png new file mode 100644 index 000000000..676a39dcf Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/delete.png differ diff --git a/swt/org.argeo.tool.swt/icons/dumpNode.gif b/swt/org.argeo.tool.swt/icons/dumpNode.gif new file mode 100644 index 000000000..14eb1be09 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/dumpNode.gif differ diff --git a/swt/org.argeo.tool.swt/icons/file.gif b/swt/org.argeo.tool.swt/icons/file.gif new file mode 100644 index 000000000..ef3028807 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/file.gif differ diff --git a/swt/org.argeo.tool.swt/icons/folder.gif b/swt/org.argeo.tool.swt/icons/folder.gif new file mode 100644 index 000000000..42e027c93 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/folder.gif differ diff --git a/swt/org.argeo.tool.swt/icons/getSize.gif b/swt/org.argeo.tool.swt/icons/getSize.gif new file mode 100644 index 000000000..b05bf3e3d Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/getSize.gif differ diff --git a/swt/org.argeo.tool.swt/icons/group.png b/swt/org.argeo.tool.swt/icons/group.png new file mode 100644 index 000000000..cc6683aff Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/group.png differ diff --git a/swt/org.argeo.tool.swt/icons/home.gif b/swt/org.argeo.tool.swt/icons/home.gif new file mode 100644 index 000000000..fd0c66950 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/home.gif differ diff --git a/swt/org.argeo.tool.swt/icons/home.png b/swt/org.argeo.tool.swt/icons/home.png new file mode 100644 index 000000000..5eb096790 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/home.png differ diff --git a/swt/org.argeo.tool.swt/icons/import_fs.png b/swt/org.argeo.tool.swt/icons/import_fs.png new file mode 100644 index 000000000..d7c890c81 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/import_fs.png differ diff --git a/swt/org.argeo.tool.swt/icons/installed.gif b/swt/org.argeo.tool.swt/icons/installed.gif new file mode 100644 index 000000000..298871653 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/installed.gif differ diff --git a/swt/org.argeo.tool.swt/icons/log.gif b/swt/org.argeo.tool.swt/icons/log.gif new file mode 100644 index 000000000..e3ecc5535 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/log.gif differ diff --git a/swt/org.argeo.tool.swt/icons/logout.png b/swt/org.argeo.tool.swt/icons/logout.png new file mode 100644 index 000000000..f2952fa5b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/logout.png differ diff --git a/swt/org.argeo.tool.swt/icons/maintenance.gif b/swt/org.argeo.tool.swt/icons/maintenance.gif new file mode 100644 index 000000000..e5690ecb1 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/maintenance.gif differ diff --git a/swt/org.argeo.tool.swt/icons/node.gif b/swt/org.argeo.tool.swt/icons/node.gif new file mode 100644 index 000000000..364c0e70b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/node.gif differ diff --git a/swt/org.argeo.tool.swt/icons/nodes.gif b/swt/org.argeo.tool.swt/icons/nodes.gif new file mode 100644 index 000000000..bba3dbc69 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/nodes.gif differ diff --git a/swt/org.argeo.tool.swt/icons/osgi_explorer.gif b/swt/org.argeo.tool.swt/icons/osgi_explorer.gif new file mode 100644 index 000000000..e9a6bd966 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/osgi_explorer.gif differ diff --git a/swt/org.argeo.tool.swt/icons/password.gif b/swt/org.argeo.tool.swt/icons/password.gif new file mode 100644 index 000000000..a6b251fc8 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/password.gif differ diff --git a/swt/org.argeo.tool.swt/icons/person-logged-in.png b/swt/org.argeo.tool.swt/icons/person-logged-in.png new file mode 100644 index 000000000..87acc1435 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/person-logged-in.png differ diff --git a/swt/org.argeo.tool.swt/icons/person.png b/swt/org.argeo.tool.swt/icons/person.png new file mode 100644 index 000000000..7d979a531 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/person.png differ diff --git a/swt/org.argeo.tool.swt/icons/query.png b/swt/org.argeo.tool.swt/icons/query.png new file mode 100644 index 000000000..54c089de1 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/query.png differ diff --git a/swt/org.argeo.tool.swt/icons/refresh.png b/swt/org.argeo.tool.swt/icons/refresh.png new file mode 100644 index 000000000..71b3481c9 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/refresh.png differ diff --git a/swt/org.argeo.tool.swt/icons/remote_connected.gif b/swt/org.argeo.tool.swt/icons/remote_connected.gif new file mode 100644 index 000000000..1492b4efa Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/remote_connected.gif differ diff --git a/swt/org.argeo.tool.swt/icons/remote_disconnected.gif b/swt/org.argeo.tool.swt/icons/remote_disconnected.gif new file mode 100644 index 000000000..6c54da9ad Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/remote_disconnected.gif differ diff --git a/swt/org.argeo.tool.swt/icons/remove.gif b/swt/org.argeo.tool.swt/icons/remove.gif new file mode 100644 index 000000000..0ae6decd0 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/remove.gif differ diff --git a/swt/org.argeo.tool.swt/icons/removePrivileges.gif b/swt/org.argeo.tool.swt/icons/removePrivileges.gif new file mode 100644 index 000000000..aa78fd2fa Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/removePrivileges.gif differ diff --git a/swt/org.argeo.tool.swt/icons/rename.gif b/swt/org.argeo.tool.swt/icons/rename.gif new file mode 100644 index 000000000..8048405a7 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/rename.gif differ diff --git a/swt/org.argeo.tool.swt/icons/repositories.gif b/swt/org.argeo.tool.swt/icons/repositories.gif new file mode 100644 index 000000000..c13bea1ca Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/repositories.gif differ diff --git a/swt/org.argeo.tool.swt/icons/repository_connected.gif b/swt/org.argeo.tool.swt/icons/repository_connected.gif new file mode 100644 index 000000000..a15fa5538 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/repository_connected.gif differ diff --git a/swt/org.argeo.tool.swt/icons/repository_disconnected.gif b/swt/org.argeo.tool.swt/icons/repository_disconnected.gif new file mode 100644 index 000000000..4576dc563 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/repository_disconnected.gif differ diff --git a/swt/org.argeo.tool.swt/icons/resolved.gif b/swt/org.argeo.tool.swt/icons/resolved.gif new file mode 100644 index 000000000..f4a1ea150 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/resolved.gif differ diff --git a/swt/org.argeo.tool.swt/icons/role.gif b/swt/org.argeo.tool.swt/icons/role.gif new file mode 100644 index 000000000..274a850e4 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/role.gif differ diff --git a/swt/org.argeo.tool.swt/icons/rollback.gif b/swt/org.argeo.tool.swt/icons/rollback.gif new file mode 100755 index 000000000..c75399599 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/rollback.gif differ diff --git a/swt/org.argeo.tool.swt/icons/save-all.png b/swt/org.argeo.tool.swt/icons/save-all.png new file mode 100644 index 000000000..b68a29b2c Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save-all.png differ diff --git a/swt/org.argeo.tool.swt/icons/save.gif b/swt/org.argeo.tool.swt/icons/save.gif new file mode 100644 index 000000000..654ad7b42 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save.gif differ diff --git a/swt/org.argeo.tool.swt/icons/save.png b/swt/org.argeo.tool.swt/icons/save.png new file mode 100644 index 000000000..f27ef2d26 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save.png differ diff --git a/swt/org.argeo.tool.swt/icons/save_security.png b/swt/org.argeo.tool.swt/icons/save_security.png new file mode 100644 index 000000000..ca41dc92b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save_security.png differ diff --git a/swt/org.argeo.tool.swt/icons/save_security_disabled.png b/swt/org.argeo.tool.swt/icons/save_security_disabled.png new file mode 100644 index 000000000..fb7d08d9a Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/save_security_disabled.png differ diff --git a/swt/org.argeo.tool.swt/icons/security.gif b/swt/org.argeo.tool.swt/icons/security.gif new file mode 100644 index 000000000..57fb95edc Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/security.gif differ diff --git a/swt/org.argeo.tool.swt/icons/service_published.gif b/swt/org.argeo.tool.swt/icons/service_published.gif new file mode 100644 index 000000000..17f771aff Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/service_published.gif differ diff --git a/swt/org.argeo.tool.swt/icons/service_referenced.gif b/swt/org.argeo.tool.swt/icons/service_referenced.gif new file mode 100644 index 000000000..c24a95fba Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/service_referenced.gif differ diff --git a/swt/org.argeo.tool.swt/icons/sort.gif b/swt/org.argeo.tool.swt/icons/sort.gif new file mode 100644 index 000000000..23c5d0b11 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/sort.gif differ diff --git a/swt/org.argeo.tool.swt/icons/starting.gif b/swt/org.argeo.tool.swt/icons/starting.gif new file mode 100644 index 000000000..563743d39 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/starting.gif differ diff --git a/swt/org.argeo.tool.swt/icons/sync.gif b/swt/org.argeo.tool.swt/icons/sync.gif new file mode 100644 index 000000000..b4fa052de Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/sync.gif differ diff --git a/swt/org.argeo.tool.swt/icons/user.gif b/swt/org.argeo.tool.swt/icons/user.gif new file mode 100644 index 000000000..90a00147b Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/user.gif differ diff --git a/swt/org.argeo.tool.swt/icons/users.gif b/swt/org.argeo.tool.swt/icons/users.gif new file mode 100644 index 000000000..2de7edd64 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/users.gif differ diff --git a/swt/org.argeo.tool.swt/icons/workgroup.png b/swt/org.argeo.tool.swt/icons/workgroup.png new file mode 100644 index 000000000..7fef996df Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workgroup.png differ diff --git a/swt/org.argeo.tool.swt/icons/workgroup.xcf b/swt/org.argeo.tool.swt/icons/workgroup.xcf new file mode 100644 index 000000000..f517c827c Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workgroup.xcf differ diff --git a/swt/org.argeo.tool.swt/icons/workspace_connected.png b/swt/org.argeo.tool.swt/icons/workspace_connected.png new file mode 100644 index 000000000..0430baaf5 Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workspace_connected.png differ diff --git a/swt/org.argeo.tool.swt/icons/workspace_disconnected.png b/swt/org.argeo.tool.swt/icons/workspace_disconnected.png new file mode 100644 index 000000000..fddcb8c4e Binary files /dev/null and b/swt/org.argeo.tool.swt/icons/workspace_disconnected.png differ diff --git a/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/CmsImages.java b/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/CmsImages.java new file mode 100644 index 000000000..1c4d79eee --- /dev/null +++ b/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/CmsImages.java @@ -0,0 +1,49 @@ +package org.argeo.cms.ui.theme; + +import java.net.URL; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +public class CmsImages { + private static BundleContext themeBc = FrameworkUtil.getBundle(CmsImages.class).getBundleContext(); + + final public static String ICONS_BASE = "icons/"; + final public static String TYPES_BASE = ICONS_BASE + "types/"; + final public static String ACTIONS_BASE = ICONS_BASE + "actions/"; + + public static Image createIcon(String name) { + return createImg(CmsImages.ICONS_BASE + name); + } + + public static Image createAction(String name) { + return createImg(CmsImages.ACTIONS_BASE + name); + } + + public static Image createType(String name) { + return createImg(CmsImages.TYPES_BASE + name); + } + + public static Image createImg(String name) { + return CmsImages.createDesc(name).createImage(Display.getDefault()); + } + + public static ImageDescriptor createDesc(String name) { + return createDesc(themeBc, name); + } + + public static ImageDescriptor createDesc(BundleContext bc, String name) { + URL url = bc.getBundle().getResource(name); + if (url == null) + return ImageDescriptor.getMissingImageDescriptor(); + return ImageDescriptor.createFromURL(url); + } + + public static Image createImg(BundleContext bc, String name) { + return createDesc(bc, name).createImage(Display.getDefault()); + } + +} diff --git a/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/package-info.java b/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/package-info.java new file mode 100644 index 000000000..7d3a260f3 --- /dev/null +++ b/swt/org.argeo.tool.swt/src/org/argeo/cms/ui/theme/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS core theme images. */ +package org.argeo.cms.ui.theme; \ No newline at end of file