From 01befa0c746c8b9b6447aca82c4ea5bd72dda08b Mon Sep 17 00:00:00 2001 From: jensp Date: Wed, 14 Oct 2015 16:57:07 +0000 Subject: [PATCH] CCM NG: Current status Admin UIs git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3691 8810af33-2d31-482b-a856-94f89814c4df --- .../java/com/arsdigita/bebop/ActionLink.java | 93 ++ .../java/com/arsdigita/bebop/Resettable.java | 41 + .../com/arsdigita/bebop/SegmentedPanel.java | 251 +++++ .../java/com/arsdigita/bebop/TabbedPane.java | 498 ++++++++++ .../main/java/com/arsdigita/bebop/Table.java | 896 ++++++++++++++++++ .../main/java/com/arsdigita/bebop/Tree.java | 762 +++++++++++++++ .../table/AbstractTableModelBuilder.java | 53 ++ .../bebop/table/DefaultTableCellRenderer.java | 159 ++++ .../bebop/table/DefaultTableColumnModel.java | 127 +++ .../bebop/table/TableCellRenderer.java | 126 +++ .../arsdigita/bebop/table/TableColumn.java | 458 +++++++++ .../bebop/table/TableColumnModel.java | 62 ++ .../arsdigita/bebop/table/TableHeader.java | 298 ++++++ .../com/arsdigita/bebop/table/TableModel.java | 104 ++ .../bebop/table/TableModelBuilder.java | 65 ++ .../bebop/tree/DefaultTreeCellRenderer.java | 70 ++ .../bebop/tree/TreeCellRenderer.java | 55 ++ .../com/arsdigita/bebop/tree/TreeModel.java | 52 + .../bebop/tree/TreeModelBuilder.java | 42 + .../com/arsdigita/bebop/tree/TreeNode.java | 47 + .../dispatcher/AccessDeniedException.java | 79 ++ .../arsdigita/toolbox/ui/ComponentMap.java | 85 ++ .../com/arsdigita/toolbox/ui/LayoutPanel.java | 86 ++ .../arsdigita/ui/admin/AdminConstants.java | 405 ++++++++ .../com/arsdigita/ui/admin/AdminServlet.java | 241 +++++ .../com/arsdigita/ui/admin/EmailList.java | 211 +++++ .../ui/admin/ExistingGroupAddPane.java | 204 ++++ .../arsdigita/ui/admin/GlobalizationUtil.java | 41 + .../com/arsdigita/ui/admin/GroupAddForm.java | 118 +++ .../ui/admin/GroupAdministrationTab.java | 685 +++++++++++++ .../com/arsdigita/ui/admin/GroupEditForm.java | 129 +++ .../com/arsdigita/ui/admin/GroupForm.java | 65 ++ .../arsdigita/ui/admin/GroupSearchForm.java | 133 +++ .../arsdigita/ui/admin/GroupTreeModel.java | 164 ++++ .../ui/admin/GroupTreeModelBuilder.java | 39 + .../arsdigita/ui/admin/PartyListModel.java | 83 ++ .../com/arsdigita/ui/admin/SearchAndList.java | 189 ++++ .../ui/admin/SearchAndListModel.java | 37 + .../com/arsdigita/ui/admin/SelectGroups.java | 166 ++++ .../arsdigita/ui/admin/SubMemberPanel.java | 173 ++++ .../ui/admin/UserAdministrationTab.java | 190 ++++ .../arsdigita/ui/admin/UserBrowsePane.java | 783 +++++++++++++++ .../com/arsdigita/ui/admin/UserEditForm.java | 144 +++ .../java/com/arsdigita/ui/admin/UserForm.java | 311 ++++++ .../arsdigita/ui/admin/UserPasswordForm.java | 286 ++++++ .../main/java/org/libreccm/core/Group.java | 17 +- .../org/libreccm/core/GroupRepository.java | 8 + .../ui/admin/AdminResources.properties | 154 +++ .../ui/admin/AdminResources_de.properties | 154 +++ .../ui/admin/AdminResources_en.properties | 112 +++ .../ui/admin/AdminResources_fr.properties | 97 ++ 51 files changed, 9841 insertions(+), 7 deletions(-) create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/ActionLink.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/Resettable.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/SegmentedPanel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/TabbedPane.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/Table.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/Tree.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/AbstractTableModelBuilder.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableCellRenderer.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableColumnModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/TableCellRenderer.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumn.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumnModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/TableHeader.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/TableModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/table/TableModelBuilder.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/tree/DefaultTreeCellRenderer.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeCellRenderer.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModelBuilder.java create mode 100644 ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeNode.java create mode 100644 ccm-core/src/main/java/com/arsdigita/dispatcher/AccessDeniedException.java create mode 100644 ccm-core/src/main/java/com/arsdigita/toolbox/ui/ComponentMap.java create mode 100644 ccm-core/src/main/java/com/arsdigita/toolbox/ui/LayoutPanel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/AdminConstants.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/AdminServlet.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/EmailList.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/ExistingGroupAddPane.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GlobalizationUtil.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAddForm.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAdministrationTab.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupEditForm.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupForm.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupSearchForm.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModelBuilder.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/PartyListModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndList.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndListModel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/SelectGroups.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/SubMemberPanel.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/UserAdministrationTab.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/UserBrowsePane.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/UserEditForm.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/UserForm.java create mode 100644 ccm-core/src/main/java/com/arsdigita/ui/admin/UserPasswordForm.java create mode 100644 ccm-core/src/main/resources/com/arsdigita/ui/admin/AdminResources.properties create mode 100644 ccm-core/src/main/resources/com/arsdigita/ui/admin/AdminResources_de.properties create mode 100755 ccm-core/src/main/resources/com/arsdigita/ui/admin/AdminResources_en.properties create mode 100755 ccm-core/src/main/resources/com/arsdigita/ui/admin/AdminResources_fr.properties diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ActionLink.java b/ccm-core/src/main/java/com/arsdigita/bebop/ActionLink.java new file mode 100644 index 000000000..b5a63603a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/ActionLink.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.globalization.GlobalizedMessage; + + + +/** + * A link that runs its action listeners when it is clicked. The target of the + * link is the {@link Page} in which the action link is contained. + * + *

Typically, an action link is used in the following way: + *

+ *   ActionLink l = new ActionLink("Send email to everybody");
+ *   l.addActionListener(new ActionListener() {
+ *      public void actionPerformed(ActionEvent e) {
+ *        System.out.println("Link was clicked.");
+ *        ... figure out who everybody is and send them email ...
+ *      }
+ *   });
+ * 
+ * + *

See {@link BaseLink} for a description of all Bebop Link classes + * and suggestions for using them. + * + * @author David Lutterkort + * @version $Id$ */ +public class ActionLink extends ControlLink { + + /** + * The value for the XML type attribute for an {@link ActionLink}. + */ + protected final String TYPE_ACTION = "action"; + + /** + * Constructs a new ActionLink. The link encapsulates + * the child component (usually either a label or an image). + * + * @param child the component to be turned into a link + */ + public ActionLink(Component child) { + super(child); + setTypeAttr(TYPE_ACTION); + } + + /** + * Constructs a new ActionLink with the given string label. + * + * @param label the string label for the link + */ + public ActionLink(GlobalizedMessage label) { + this(new Label(label)); + } + + /** + * Constructs a new ActionLink with the given string label. + * + * @param label the string label for the link + * @deprecated refactor to use @see ActionLink(GlobalizedMessage label) + */ + public ActionLink(String label) { + this(new Label(label)); + } + + /** + * Sets the page state's control event. Should be overridden by child + * classes. By default, the link does not receive any control events. + * + * @param s the current page state + */ + @Override + public void setControlEvent(PageState s) { + s.setControlEvent(this); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Resettable.java b/ccm-core/src/main/java/com/arsdigita/bebop/Resettable.java new file mode 100644 index 000000000..685678ed5 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Resettable.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import com.arsdigita.bebop.PageState; + +/** + * + * Interface that should be implemented by components that have + * state parameters that need to be reset when the component is shown + * to the user. The details of when the reset method is called are left + * to the application programmer. + * + * @version $Id$ + */ +public interface Resettable { + + /** + * Resets the state of the component to its original + * appearance. + * + * @param state the page state + */ + void reset(PageState state); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SegmentedPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/SegmentedPanel.java new file mode 100644 index 000000000..30f4da27a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SegmentedPanel.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import static com.arsdigita.bebop.Component.*; +import static com.arsdigita.bebop.util.BebopConstants.*; + +import com.arsdigita.xml.Element; +import com.arsdigita.util.Assert; + +import com.arsdigita.bebop.util.BebopConstants; + +/** + * Generates a list of segments. Each segment consists of a header + * (which could be any Bebop component) and a body (which, likewise, + * could be any component). The entire SegmentedPanel + * look roughly like this: + * + *


+ * ----------------------
+ * Header 1
+ * ----------------------
+ * Body 1
+ * More Body 1
+ * Even more Body 1
+ * ----------------------
+ * Header 2
+ * ----------------------
+ * Body 2
+ * More Body 2
+ * Even more Body 2
+ * 
+ * + * Typically, the body of each segment will be a {@link SimpleContainer} + * which contains many other components + *

+ * The XML generated by this component looks something like this: + *


+ * <bebop:segmentedPanel>
+ *   <bebop:segment id="foo">
+ *     <bebop:segmentHeader>
+ *       <aRandomHeaderComponent/>
+ *       <anotherRandomHeaderComponent/>
+ *       ...
+ *     </bebop:segmentHeader>
+ *     <bebop:segmentBody>
+ *       <aRandomBodyComponent>
+ *       <anotherRandomBodyComponent>
+ *       ...
+ *     </bebop:segmentBody>
+ *   </bebop:segment>
+ *   ...
+ * </bebop:segmentedPanel>
+ * 
+ * + * @see #generateXML(PageState, Element) + * + * @author Michael Pih + * @version $Id$ + */ +public class SegmentedPanel extends SimpleContainer + implements BebopConstants { + + public static final String HEADER_CLASS = "seg-header"; + + /** + * Construct a new SegmentedPanel + */ + public SegmentedPanel() { + super(); + } + + /** + * Construct a new SegmentedPanel + * + * @param idAttr the XSL ID attribute for this container + * @see SimpleComponent#setIdAttr(String) + */ + public SegmentedPanel(String idAttr) { + this(); + setIdAttr(idAttr); + } + + /** + * Add a segment to this container + * + * @param header the component that will act as the header + * @param body the component that will act as the body + * @return the new segment + */ + public Segment addSegment(Component header, Component body) { + Segment s = new Segment(header, body); + add(s); + return s; + } + + /** + * Add a segment to this container. + * + * @param segmentID the XSL ID attribute for the new segment. + * @param header the component that will act as the header. + * @param body the component that will act as the body + * @return the new segment + */ + public Segment addSegment(String segmentID, Component header, Component body) { + Segment s = addSegment(header, body); + s.setIdAttr(segmentID); + return s; + } + + /** + * Add a segment to this container. + * + * @param segmentID the XSL ID attribute for the new segment. The XSL template + * for this component will render the correct header based on the ID attribute + * @param body the component that will act as the body + * @return the new segment + */ + public Segment addSegment(String segmentID, Component body) { + return addSegment(segmentID, null, body); + } + + /** + * Add and return a new empty segment. + * + * @return a new empty segment that is part of the panel. + */ + public Segment addSegment() { + Segment result = new Segment(); + add(result); + return result; + } + + /** + * Generate the XML for this component, as described above + * + * @param state represents the page state for the current request + * @param parent the parent XML element + */ + public void generateXML(PageState state, Element parent) { + if ( isVisible(state) ) { + Element panel = parent.newChildElement(BEBOP_SEG_PANEL, BEBOP_XML_NS); + exportAttributes(panel); + super.generateXML(state, panel); + } + } + + /** + * A single Segment within this container + */ + public static class Segment extends SimpleContainer { + + private SimpleContainer m_header, m_body; + + /** + * Construct an empty Segment + */ + public Segment() { + this(null, null); + } + + /** + * Construct a new Segment + * + * @param header the component which will act as the header; the XSL + * class attribute for the component will be set to {@link #HEADER_CLASS}. + * Typically, this component will be a {@link Label} + * @param body the component which represents the body of the segment, Typically, + * this component will be a {@link SimpleContainer} or a panel of some sort + */ + public Segment(Component header, Component body) { + super(); + if(header != null) addHeader(header); + if(body!= null) add(body); + } + + /** + * Construct a new Segment with no header + * + * @param body the component which represents the body of the segment, Typically, + * this component will be a {@link SimpleContainer} or a panel of some sort + */ + public Segment(Component body) { + this(null, body); + } + + /** + * Add a header component. + * + * @param c an additional header component + */ + public void addHeader(Component c) { + Assert.isUnlocked(this); + if(m_header == null) { + m_header = new SimpleContainer(BEBOP_SEG_HEADER, BEBOP_XML_NS); + super.add(m_header); + } + m_header.add(c); + } + + /** + * Add a component to the body of this segment + */ + public void add(Component c) { + Assert.isUnlocked(this); + if(m_body == null) { + m_body = new SimpleContainer(BEBOP_SEG_BODY, BEBOP_XML_NS); + super.add(m_body); + } + m_body.add(c); + } + + /** + * Add a component to the body of this segment + */ + public void add(Component c, int constraints) { + add(c); + } + + /** + * Generate the XML for this segment + * + * @param state the current page state + * @param parent the parent XML element + */ + public void generateXML(PageState state, Element parent) { + if(isVisible(state)) { + Element seg = parent.newChildElement(BEBOP_SEGMENT, BEBOP_XML_NS); + exportAttributes(seg); + super.generateXML(state, seg); + } + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/TabbedPane.java b/ccm-core/src/main/java/com/arsdigita/bebop/TabbedPane.java new file mode 100644 index 000000000..50b93896d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/TabbedPane.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import static com.arsdigita.bebop.Component.*; + +import com.arsdigita.bebop.parameters.IntegerParameter; +import com.arsdigita.util.Assert; +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.xml.Element; + +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; + +import javax.servlet.ServletException; + +import org.apache.log4j.Logger; + +/* FIXME: Add methods for using images in the tab strip */ + +/** + * A tabbed pane that lets the user switch between components by + * clicking on a given title in the tab strip. + *

+ * Tabs (components) are added using the {@link #addTab addTab} method. Each + * entry consists of a label (which is a string) and the {@link Component} + * that is displayed if the user clicks on the label. + *

+ * There is always exactly one component that is currently visible, the component + * that is returned by {@link #getCurrentPane}. Without user interaction, + * this is the default pane -- that was set by {@link #setDefaultPane} -- or, if + * none has been set, the first component that was added to the TabbedPane. + *

+ * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Uday Mathur + * @version $Id$ + */ +public class TabbedPane extends SimpleContainer { + + private static final String CURRENT_PANE = "pane"; + /** + * The name for the event to change the selected pane. + * The value is the index of the pane + */ + private static final String SELECT_EVENT = "select"; + + private Pane m_defaultPane; + private IntegerParameter m_currentPaneParam; + private List m_actionListeners; + + private static final Logger s_log = + Logger.getLogger(TabbedPane.class.getName()); + + /** + * Constructs an empty TabbedPane. + */ + public TabbedPane() { + m_currentPaneParam = new IntegerParameter(CURRENT_PANE); + } + + /** + * Registers with the specified root container. Adds a state + * parameter to keep track of the visible component to the page. + * @param p the root container to register with + * @pre p != null + */ + public void register(Page p) { + Assert.isUnlocked(this); + + p.addComponentStateParam(this, m_currentPaneParam); + // if there is no default pane, then set it to the first one + // in the list + Iterator i = children(); + if (!i.hasNext()) { + s_log.warn("TabbedPane registered with no panes"); + } else if (m_defaultPane == null) { + setDefaultPaneIndex(0); + } + while (i.hasNext()) { + Pane pane = (Pane) i.next(); + p.setVisibleDefault(pane.getComponent(), pane == m_defaultPane); + } + } + + /** + * Adds a new pane to the dialog. Assigns a rather unhelpful default label + * (the pane number) to the component. Use {@link #addTab addTab} + * instead. + * + * @pre pc != null + */ + public void add(Component pc) { + addTab(String.valueOf(size()), pc); + } + + /** + * Adds a new pane with layout constraints to the dialog. Ignores + * the constraints. Assigns a rather unhelpful default label + * (the pane number) to the component. Use {@link #addTab + * addTab} instead. + * + * @pre pc != null */ + public void add(Component pc, int constraints) { + add(pc); + } + + /** + * Adds a tab and its associated component. + * @param label the text to display in the tab strip + * @param c the component to display when the user clicks on the + * label in the tab strip + * + * @pre label != null && c != null + */ + public void addTab(Component label, Component c) { + Assert.isUnlocked(this); + super.add(new Pane(label, c)); + } + + /** + * Adds a tab and its associated component. + * @param label the text to display in the tab strip + * @param c the component to display when the user clicks on the + * label in the tab strip + * + * @pre label != null && c != null + */ + public void addTab(String label, Component c) { + addTab(new Label(label), c); + } + + /** + * Adds an ActionListener, which is run whenever {@link + * #respond respond} is called. + * @param 1 the action listener + * + * @pre l != null + * @pre ! isLocked() + * @see #respond respond + */ + public void addActionListener(ActionListener l) { + Assert.isUnlocked(this); + if ( m_actionListeners == null ) { + m_actionListeners = new ArrayList(); + } + m_actionListeners.add(l); + } + + /** + * Removes a previously added ActionListener. + * @param 1 the action listener to remove + * @see #addActionListener addActionListener + */ + public void removeActionListener(ActionListener l) { + Assert.isUnlocked(this); + if ( m_actionListeners == null ) { + return; + } + m_actionListeners.remove(l); + } + + /** + * Fires an ActionEvent. All registered + * ActionListeners are run. The source of the event is the + * TabbedPane. + * @param state the current page state + * @pre state != null + * @see #respond respond + */ + protected void fireActionEvent(PageState state) { + ActionEvent e = null; + if (m_actionListeners == null) { + return; + } + for (Iterator i=m_actionListeners.iterator(); i.hasNext(); ) { + if ( e == null ) { + e = new ActionEvent(this, state); + } + ((ActionListener) i.next()).actionPerformed(e); + } + } + + /** + * Sets the index of the default pane, which is visible until the user + * clicks on another label in the tab strip. + * @param i the index of the default pane + */ + protected void setDefaultPaneIndex(int i) { + m_currentPaneParam.setDefaultValue(new Integer(i)); + m_defaultPane = (Pane)get(i); + } + + /** + * Sets the default pane, which is visible until the user + * clicks on another label in the tab strip. + * @param pane the component to display as the default pane + * + * @pre findPane(pane) != -1 + */ + public void setDefaultPane(Component pane) + throws IllegalArgumentException { + Assert.isUnlocked(this); + + setDefaultPaneIndex(findPaneSafe(pane)); + } + + /** + * Show or hide a particular tab + * + * @param s the page state + * @param i the index of the tab + * @param v if true, shows the tab. Otherwise, hides the tab + */ + public void setTabVisible(PageState s, int i, boolean v) { + get(i).setVisible(s, v); + } + + /** + * Show or hide a particular tab + * + * @param s the page state + * @param c the body of the tab + * @param v if true, shows the tab. Otherwise, hides the tab + */ + public void setTabVisible(PageState s, Component c, boolean v) { + int i = findPaneSafe(c); + setTabVisible(s, i, v); + } + + /** + * Determine if a particular tab is visible + * + * @param s the page state + * @param i the index of the tab + */ + public boolean isTabVisible(PageState s, int i) { + return get(i).isVisible(s); + } + + /** + * Determine if a particular tab is visible + * + * @param s the page state + * @param c the body of the tab + */ + public boolean isTabVisible(PageState s, Component c) { + int i = findPaneSafe(c); + return isTabVisible(s, i); + } + + /** + * Find the pane whose body is the specified component + * @param c the component + * @return the pane index on success, -1 if no such pane exists + */ + protected int findPane(Component c) { + int index = 0; + for(Iterator i = children(); i.hasNext(); index++) { + Pane p = (Pane)i.next(); + if(p.getComponent() == c) + return index; + } + + return -1; + } + + private int findPaneSafe(Component c) { + int i = findPane(c); + if ( i == -1 ) { + throw new IllegalArgumentException + ("Pane not part of this tabbed dialog"); + } + + return i; + } + + + /** + * Gets the default pane. If no default pane has been set explicitly, the + * first pane is returned. + * + * @return the default pane, or null if there are no + * panes. + */ + public Component getDefaultPane() { + return m_defaultPane.getComponent(); + } + + /** + * Gets the pane with the specified label. + * @return the pane with the specified label, or null + * if a pane with that label does not exist. + */ + public Component getPane(Component label) { + for (Iterator i = children(); i.hasNext();) { + Pane p = (Pane) i.next(); + if ( p.getLabel().equals(label) ) { + return p.getComponent(); + } + } + return null; + } + + /** + * Gets the pane with the specified key in its label. + * Returns null if a pane with that label does not exist. + * This function exists for backward compatibility. + * @return the pane with the specified label, or null + * if a pane with that label does not exist. + */ + public Component getPane(String label) { + + for (Iterator i = children(); i.hasNext();) { + Pane p = (Pane) i.next(); + Component pLabel = p.getLabel(); + if (pLabel instanceof Label + && ((Label)pLabel).getLabel().equals(label) ) { + return p.getComponent(); + } + } + return null; + } + + /** + * Gets the currently visible pane. + * + * @pre data != null + */ + public Component getCurrentPane(PageState data) { + return getCurrent(data).getComponent(); + } + + /** + * Get the currently visible Pane, the tab label together + * with its component. + */ + private Pane getCurrent(PageState data) { + Integer i = (Integer) data.getValue(m_currentPaneParam); + if (i == null) { + if (m_defaultPane!=null) { + + return m_defaultPane; + } else { + return (Pane)get(0); + } + } + return (Pane)get(i.intValue()); + } + + public void setSelectedIndex(PageState state, int index) { + if ( index != getSelectedIndex(state) ) { + getCurrentPane(state).setVisible(state, false); + state.setValue(m_currentPaneParam, new Integer(index)); + getCurrentPane(state).setVisible(state, true); + } + } + + public int getSelectedIndex(PageState state) { + Integer current = (Integer) state.getValue(m_currentPaneParam); + if ( current == null ) { + return -1; + } + return current.intValue(); + } + + + /** + * Builds a DOM representing the header for the tab strip. Marks the current pane. + */ + protected void generateTabs(PageState data, Element parent) { + Element strip = parent.newChildElement("bebop:tabStrip", BEBOP_XML_NS); + exportAttributes(strip); + + Pane current = getCurrent(data); + strip.addAttribute("selected",current.getComponent().getClass().getName()); + Iterator tabs; + int i; + for (tabs = children(), i = 0; tabs.hasNext(); i++) { + Pane pane = (Pane)tabs.next(); + // Skip hidden tabs + if(!pane.isVisible(data)) continue; + + data.setControlEvent(this, SELECT_EVENT, String.valueOf(i)); + + Element tab = strip.newChildElement("bebop:tab", BEBOP_XML_NS); + if (pane == current) { + tab.addAttribute("current", "t"); + } else { + try { + tab.addAttribute("href", data.stateAsURL()); + } catch (java.io.IOException ioe) { + // stateAsURL failed => this node gets neither href nor current + //TODO cat.error("cannot get stateAsURL from "+data); + } + } + String key = ((Label) pane.getLabel()).getGlobalizedMessage().getKey(); + tab.addAttribute("key", key.substring(key.lastIndexOf(".") + 1)); + pane.getLabel().generateXML(data, tab); + } + data.clearControlEvent(); + } + + /** + * Services the request by building a DOM tree with the tabs + * themselves and then the included page. + *

Generates a DOM fragment: + *

+     * <bebop:tabbedPane>
+     *  <bebop:tabStrip>
+     *   <bebop:tab [href="..."] [current="t|f"]> .. label .. </bebop:tab>
+     *   <bebop:tab [href="..."] [current="t|f"]> .. label .. </bebop:tab>
+     *   <bebop:tab [href="..."] [current="t|f"]> .. label .. </bebop:tab>
+     *  </bebop:tabStrip>
+     *  <bebop:currentPane>
+     *    ... contentes ..
+     *  </bebop:currentPane>
+     * </bebop:tabbedPane>
+     * 
+ */ + public void generateXML(PageState state, Element parent) { + if ( isVisible(state) && !isEmpty()) { + Element tabbed = parent.newChildElement("bebop:tabbedPane", BEBOP_XML_NS); + generateTabs(state, tabbed); + exportAttributes(tabbed); + + Element pane = tabbed.newChildElement("bebop:currentPane", BEBOP_XML_NS); + exportAttributes(pane); + getCurrentPane(state).generateXML(state, pane); + } + } + + /** + * Notifies the TabbedPane that one of the tabs has been + * selected. Changes the currently visible pane and runs all the {@link + * ActionListener ActionListeners}. + *

+ * The respond method on the now-visible component is + * not called. + * + * @pre state != null + */ + public void respond(PageState state) + throws ServletException + { + String event = state.getControlEventName(); + + if ( SELECT_EVENT.equals(event)) { + String value = state.getControlEventValue(); + setSelectedIndex(state, Integer.parseInt(value)); + } else { + throw new ServletException("Received unknown control event " + event); + } + fireActionEvent(state); + } + + /** + * Associates a label with the component + */ + private class Pane extends SimpleContainer { + private Component m_label; + private Component m_component; + + public Pane(Component label, Component c) { + m_label = label; + super.add(label); + m_component = c; + super.add(c); + } + + public final Component getLabel() { + return m_label; + } + + public final Component getComponent() { + return m_component; + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Table.java b/ccm-core/src/main/java/com/arsdigita/bebop/Table.java new file mode 100644 index 000000000..8e863c3b3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/Table.java @@ -0,0 +1,896 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import static com.arsdigita.bebop.Component.*; + +import com.arsdigita.bebop.event.EventListenerList; +import com.arsdigita.bebop.event.TableActionAdapter; +import com.arsdigita.bebop.event.TableActionEvent; +import com.arsdigita.bebop.event.TableActionListener; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.bebop.table.AbstractTableModelBuilder; +import com.arsdigita.bebop.table.DefaultTableCellRenderer; +import com.arsdigita.bebop.table.DefaultTableColumnModel; +import com.arsdigita.bebop.table.TableCellRenderer; +import com.arsdigita.bebop.table.TableColumn; +import com.arsdigita.bebop.table.TableColumnModel; +import com.arsdigita.bebop.table.TableHeader; +import com.arsdigita.bebop.table.TableModel; +import com.arsdigita.bebop.table.TableModelBuilder; + +import static com.arsdigita.bebop.util.BebopConstants.*; + +import com.arsdigita.bebop.util.BebopConstants; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +import java.util.Iterator; + +import javax.servlet.ServletException; + +import org.apache.log4j.Logger; + +/** + * Displays statically or dynamically generated data in tabular form. + * The data is retrieved from a TableModel. + * + *

+ * This class is similar to the {@link List} class, but it has two dimensions. + * The table consists of a {@link TableModelBuilder}, a {@link TableColumnModel}, + * a {@link TableHeader} and a {@link TableCellRenderer} for each column. + *

+ * + * A table that represents a static matrix can be created fairly quickly: + *

 String[][] data = {
+ *   {"Stas", "Freidin"},
+ *   {"David", "Lutterkort"}
+ * };
+ *
+ * String[] headers = {"First Name", "Last Name"};
+ *
+ * Table myTable = new Table(data, headers);
+ *

+ * + * However, tables are most often used to represent database queries, not static + * data. For these tables, the {@link TableModelBuilder} class should be used + * to supply the Table class with data. + * The {@link TableModelBuilder} class will execute the database query and + * return a {@link TableModel}, which wraps the query. + *

+ * + * The content in the cells is rendered by the {@link TableCellRenderer} that + * is set for the {@link TableColumn} to which the cell belongs. + * + * If the TableCellRenderer has not been set, the + * TableCellRenderer for the Table is used. + * By default, the Table class uses an inactive instance of the + * {@link DefaultTableCellRenderer} (cell content is displayed as {@link Label}s). + * However, if an active DefaultTableCellRenderer is used, the + * cells in the table appear as links. When the user clicks a link, the + * Table's action listeners will be fired. + * + *

+ * The currently selected cell is represented by two {@link SingleSelectionModel}s - + * one model for the row and one model for the column. Typically, the selected + * row is identified by a string key and the selected column is identified by + * an integer. + * + * @see TableModel + * @see TableColumnModel + * + * @author David Lutterkort + * @version $Id$ + */ +public class Table extends SimpleComponent { + + private static final Logger logger = Logger.getLogger(Table.class); + // Names for HTML Attributes + private static final String WIDTH = "width"; + private static final String CELL_SPACING = "cellspacing"; + private static final String CELL_PADDING = "cellpadding"; + private static final String BORDER = "border"; + private static final String SELECTED_ROW = "row"; + /** + * The control event when the user selects one table cell. + * This control event will only be used when + */ + protected static final String CELL_EVENT = "cell"; + protected static final char SEP = ' '; + private TableModelBuilder m_modelBuilder; + private TableColumnModel m_columnModel; + private TableHeader m_header; + private RequestLocal m_tableModel; + private SingleSelectionModel m_rowSelectionModel; + /** + * A listener to forward headSelected events originating from the + * TableHeader. This will be null until somebody actually registers a + * TableActionListener from the outside. + */ + private TableActionListener m_headerForward; + private EventListenerList m_listeners; + private TableCellRenderer m_defaultCellRenderer; + private Component m_emptyView; + private boolean m_striped = false; + + /** + * Constructs a new, empty table. + */ + public Table() { + this(new Object[0][0], new Object[0]); + } + + /** + * Constructs a static table with the specified column headers, + * and pre-fills it with data. + * + * @param data a matrix of objects that will serve as static data + * for the table cells + * + * @param headers an array of string labels for the table headers + */ + public Table(Object[][] data, Object[] headers) { + this(new MatrixTableModelBuilder(data), headers); + } + + /** + * Constructs a table using a {@link TableModelBuilder}. The table + * data will be generated dynamically during each request. + * + * @param b the {@link TableModelBuilder} that is responsible for + * instantiating a {@link TableModel} during each request + * + * @param headers an array of string labels for the table headers + */ + public Table(TableModelBuilder b, Object[] headers) { + this(b, new DefaultTableColumnModel(headers)); + } + + /** + * Constructs a table using a {@link TableModelBuilder}. The table + * data will be generated dynamically during each request. The + * table's columns and headers will be provided by a + * {@link TableColumnModel}. + * + * @param b the {@link TableModelBuilder} that is responsible for + * instantiating a {@link TableModel} during each request + * + * @param c the {@link TableColumnModel} that will maintain the + * columns and headers for this table + */ + public Table(TableModelBuilder b, TableColumnModel c) { + super(); + m_modelBuilder = b; + m_columnModel = c; + setHeader(new TableHeader(m_columnModel)); + m_rowSelectionModel = + new ParameterSingleSelectionModel(new StringParameter(SELECTED_ROW)); + m_listeners = new EventListenerList(); + m_defaultCellRenderer = new DefaultTableCellRenderer(); + initTableModel(); + } + + // Events and listeners + + /** + * Adds a {@link TableActionListener} to the table. The listener is + * fired whenever a table cell is clicked. + * + * @param l the {@link TableActionListener} to be added + */ + public void addTableActionListener(TableActionListener l) { + Assert.isUnlocked(this); + if (m_headerForward == null) { + m_headerForward = createTableActionListener(); + if (m_header != null) { + m_header.addTableActionListener(m_headerForward); + } + } + m_listeners.add(TableActionListener.class, l); + } + + /** + * Removes a {@link TableActionListener} from the table. + * + * @param l the {@link TableActionListener} to be removed + */ + public void removeTableActionListener(TableActionListener l) { + Assert.isUnlocked(this); + m_listeners.remove(TableActionListener.class, l); + } + + /** + * Fires event listeners to indicate that a new cell has been + * selected in the table. + * + * @param state the page state + * @param rowKey the key that identifies the selected row + * @param column the integer index of the selected column + */ + protected void fireCellSelected(PageState state, + Object rowKey, Integer column) { + Iterator i = m_listeners.getListenerIterator(TableActionListener.class); + TableActionEvent e = null; + + while (i.hasNext()) { + if (e == null) { + e = new TableActionEvent(this, state, rowKey, column); + } + ((TableActionListener) i.next()).cellSelected(e); + } + } + + /** + * Fires event listeners to indicate that a new header cell has been + * selected in the table. + * + * @param state the page state + * @param rowKey the key that identifies the selected row + * @param column the integer index of the selected column + */ + protected void fireHeadSelected(PageState state, + Object rowKey, Integer column) { + Iterator i = m_listeners.getListenerIterator(TableActionListener.class); + TableActionEvent e = null; + + while (i.hasNext()) { + if (e == null) { + e = new TableActionEvent(this, state, rowKey, column); + } + ((TableActionListener) i.next()).headSelected(e); + } + } + + /** + * Instantiates a new {@link TableActionListener} for this table. + * + * @return a new {@link TableActionListener} that should be used + * only for this table. + * + */ + protected TableActionListener createTableActionListener() { + return new TableActionAdapter() { + @Override + public void headSelected(TableActionEvent e) { + fireHeadSelected(e.getPageState(), e.getRowKey(), e.getColumn()); + } + }; + } + + /** + * @return the {@link TableColumnModel} for this table. + */ + public final TableColumnModel getColumnModel() { + return m_columnModel; + } + + /** + * Sets a new {@link TableColumnModel} for the table. + * + * @param v the new {@link TableColumnModel} + */ + public void setColumnModel(TableColumnModel v) { + Assert.isUnlocked(this); + m_columnModel = v; + } + + /** + * @return the {@link TableModelBuilder} for this table. + */ + public final TableModelBuilder getModelBuilder() { + return m_modelBuilder; + } + + /** + * Sets a new {@link TableModelBuilder} for the table. + * + * @param v the new {@link TableModelBuilder} + */ + public void setModelBuilder(TableModelBuilder v) { + Assert.isUnlocked(this); + m_modelBuilder = v; + } + + /** + * @return the {@link TableHeader} for this table. Could return null + * if the header is hidden. + */ + public final TableHeader getHeader() { + return m_header; + } + + /** + * Sets a new header for this table. + * + * @param v the new header for this table. If null, the header will be + * hidden. + */ + public void setHeader(TableHeader v) { + Assert.isUnlocked(this); + if (m_headerForward != null) { + if (m_header != null) { + m_header.removeTableActionListener(m_headerForward); + } + if (v != null) { + v.addTableActionListener(m_headerForward); + } + } + m_header = v; + if (m_header != null) { + m_header.setTable(this); + } + } + + /** + * @param i the numerical index of the column + * @return the {@link TableColumn} whose index is i. + */ + public TableColumn getColumn(int i) { + return getColumnModel().get(i); + } + + /** + * Maps the colulumn at a new numerical index. This method + * is normally used to rearrange the order of the columns in the + * table. + * + * @param i the numerical index of the column + * @param v the column that is to be mapped at i + */ + public void setColumn(int i, TableColumn v) { + getColumnModel().set(i, v); + } + + /** + * @return the {@link SingleSelectionModel} that is responsible + * for selecting the current row. + */ + public final SingleSelectionModel getRowSelectionModel() { + return m_rowSelectionModel; + } + + /** + * Specifies the {@link SingleSelectionModel} that will be responsible + * for selecting the current row. + * + * @param v a {@link SingleSelectionModel} + */ + public void setRowSelectionModel(SingleSelectionModel v) { + Assert.isUnlocked(this); + m_rowSelectionModel = v; + } + + /** + * @return the {@link SingleSelectionModel} that is responsible + * for selecting the current column. + */ + public SingleSelectionModel getColumnSelectionModel() { + return (getColumnModel() == null) ? null : getColumnModel(). + getSelectionModel(); + } + + /** + * Specifies the {@link SingleSelectionModel} that will be responsible + * for selecting the current column. + * + * @param v a {@link SingleSelectionModel} + */ + public void setColumnSelectionModel(SingleSelectionModel v) { + Assert.isUnlocked(this); + // TODO: make sure table gets notified of changes + getColumnModel().setSelectionModel(v); + } + + /** + * Clears the row and column selection models that the table holds. + * + * @param s represents the state of the current request + * @post ! getRowSelectionModel().isSelected(s) + * @post ! getColumnSelectionModel().isSelected(s) + */ + public void clearSelection(PageState s) { + getRowSelectionModel().clearSelection(s); + getColumnSelectionModel().clearSelection(s); + } + + /** + * @return the default {@link TableCellRenderer}. + */ + public final TableCellRenderer getDefaultCellRenderer() { + return m_defaultCellRenderer; + } + + /** + * Specifies the default cell renderer. This renderer will + * be used to render columns that do not specify their own + * {@link TableCellRenderer}. + * + * @param v the default {@link TableCellRenderer} + */ + public final void setDefaultCellRenderer(TableCellRenderer v) { + m_defaultCellRenderer = v; + } + + /** + * @return the component that will be shown if the table is + * empty. + */ + public final Component getEmptyView() { + return m_emptyView; + } + + /** + * Sets the empty view. The empty view is the component that + * is shown if the table is empty. Usually, the component + * will be a simple label, such as new Label("The table is empty"). + * + * @param v a Bebop component + */ + public final void setEmptyView(Component v) { + m_emptyView = v; + } + + // Set HTML table attributes + /** + * + * @return the HTML width of the table. + */ + public String getWidth() { + return getAttribute(WIDTH); + } + + /** + * + * @param v the HTML width of the table + */ + public void setWidth(String v) { + setAttribute(WIDTH, v); + } + + /** + * + * @return the HTML border of the table. + */ + public String getBorder() { + return getAttribute(BORDER); + } + + /** + * + * @param v the HTML border of the table + */ + public void setBorder(String v) { + setAttribute(BORDER, v); + } + + public String getCellSpacing() { + return getAttribute(CELL_SPACING); + } + + /** + * + * @param v the HTML width of the table + */ + public void setCellSpacing(String v) { + setAttribute(CELL_SPACING, v); + } + + /** + * + * @return the HTML cell spacing of the table. + */ + public String getCellPadding() { + return getAttribute(CELL_PADDING); + } + + /** + * + * @param v the HTML cell padding of the table + */ + public void setCellPadding(String v) { + setAttribute(CELL_PADDING, v); + } + + /** + * Processes the events for this table. This method will automatically + * handle all user input to the table. + * + * @param s the page state + * @throws javax.servlet.ServletException + */ + @Override + public void respond(PageState s) throws ServletException { + String event = s.getControlEventName(); + String rowKey = null; + Integer column = null; + + if (CELL_EVENT.equals(event)) { + String value = s.getControlEventValue(); + SingleSelectionModel rowSel = getRowSelectionModel(); + SingleSelectionModel colSel = getColumnSelectionModel(); + int split = value.indexOf(SEP); + rowKey = value.substring(0, split); + column = new Integer(value.substring(split + 1)); + colSel.setSelectedKey(s, column); + rowSel.setSelectedKey(s, rowKey); + fireCellSelected(s, rowKey, column); + } else { + throw new ServletException("Unknown event '" + event + "'"); + } + } + + /** + * Registers the table with the containing page. The table will add the + * state parameters of the row and column selection models, if they use + * them, thus making the selection persist between requests. + * + * @param p the page that contains this table + */ + @Override + public void register(Page p) { + ParameterModel m = getRowSelectionModel() == null ? null + : getRowSelectionModel().getStateParameter(); + if (m != null) { + p.addComponentStateParam(this, m); + } + m = getColumnSelectionModel() == null ? null : getColumnSelectionModel(). + getStateParameter(); + if (m != null) { + p.addComponentStateParam(this, m); + } + } + + /** + * Returns an iterator over the header and all the columns. If the table + * has no header, the iterator lists only the columns. + * + * @return an iterator over Bebop components. + */ + @Override + public Iterator children() { + return new Iterator() { + + int pos = (getHeader() == null) ? -1 : -2; + + @Override + public boolean hasNext() { + return pos < getColumnModel().size() - 1; + } + + @Override + public Object next() { + pos += 1; + if (pos == -1) { + return getHeader(); + } else { + return getColumn(pos); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read-only iterator."); + } + }; + } + + /** + * Determines whether a row is seleted. + * + * @param s the page state + * @param rowKey the key that identifies the row + * @return true if the row is currently selected; + * false otherwise. + */ + public boolean isSelectedRow(PageState s, Object rowKey) { + if (rowKey == null || getRowSelectionModel() == null) { + return false; + } + return getRowSelectionModel().isSelected(s) + && rowKey.toString().equals( + getRowSelectionModel().getSelectedKey(s).toString()); + } + + /** + * Determines whether a column is selected. + * + * @param s the page state + * @param column a key that identifes the column. Should be consistent + * with the type used by the column selection model. + * @return true if the column is selected; + * false otherwise. + */ + public boolean isSelectedColumn(PageState s, Object column) { + if (column == null || getColumnSelectionModel() == null) { + return false; + } + return getColumnSelectionModel().isSelected(s) + && column.toString().equals( + getColumnSelectionModel().getSelectedKey(s).toString()); + } + + /** + * Determines whether the cell addressed by the specified row key and + * column number is selected in the request represented by the page + * state. + * + * @param s represents the state of the page in the current request + * @param rowKey the row key of the cell. The concrete type should agree + * with the type used by the row selection model. + * @param column the column of the cell. The concrete type should agree + * with the type used by the column selection model. + * @return true if the cell is selected; + * false otherwise. + */ + public boolean isSelectedCell(PageState s, Object rowKey, Object column) { + return isSelectedRow(s, rowKey) && isSelectedColumn(s, column); + } + + public void setStriped(boolean striped) { + m_striped = striped; + } + + public boolean getStriped() { + return m_striped; + } + + /** + * Adds type-specific XML attributes to the XML element representing + * this link. Subclasses should override this method if they introduce + * more attributes than the ones {@link #generateXML generateXML} + * produces by default. + * + * @param state represents the current request + * @param element the XML element representing this table + */ + protected void generateExtraXMLAttributes(PageState state, + Element element) { + } + + /** + * Generates the XML representing the table. Gets a new {@link TableModel} + * from the {@link TableModelBuilder} and iterates over the model's + * rows. The value in each table cell is rendered with the help of the + * column's table cell renderer. + * + *

Generates an XML fragment: + *

+     * <bebop:table>
+     *   <bebop:thead>
+     *     <bebpp:cell>...</cell> ...
+     *   </bebop:thead>
+     *   <bebop:tbody>
+     *     <bebop:trow>
+     *       <bebpp:cell>...</cell> ...
+     *     </bebop:trow>
+     *       ...
+     *   </bebop:tbody>
+     * </bebop:table>
+     *
+     * @param s the page state
+     * @param p the parent {@link Element}
+     */
+    @Override
+    public void generateXML(PageState s, Element p) {
+        TableModel model = getTableModel(s);
+
+
+        final boolean tableIsRegisteredWithPage =
+                      s.getPage().stateContains(getControler());
+
+        if (model.nextRow()) {
+            Element table = p.newChildElement(BEBOP_TABLE, BEBOP_XML_NS);
+            exportAttributes(table);
+            generateExtraXMLAttributes(s, table);
+            if (getHeader() != null) {
+                getHeader().generateXML(s, table);
+            }
+            Element tbody = table.newChildElement(BEBOP_TABLEBODY, BEBOP_XML_NS);
+            if (m_striped) {
+                tbody.addAttribute("striped", "true");
+            }
+
+            final int modelSize = getColumnModel().size();
+            int row = 0;
+
+            logger.debug("Creating table rows...");
+            long start = System.currentTimeMillis();
+            do {
+                long rowStart = System.currentTimeMillis();
+                Element trow = tbody.newChildElement(BEBOP_TABLEROW,
+                                                     BEBOP_XML_NS);
+
+                for (int i = 0; i < modelSize; i++) {
+
+                    TableColumn tc = getColumn(i);
+                    if (tc.isVisible(s)) {
+                        TableCellRenderer r = tc.getCellRenderer();
+                        if (r == null) {
+                            r = m_defaultCellRenderer;
+                        }
+                        final int modelIndex = tc.getModelIndex();
+                        Object key = model.getKeyAt(modelIndex);
+                        Object value = model.getElementAt(modelIndex);
+                        boolean selected =
+                                isSelectedCell(s, key, new Integer(i));
+                        if (tableIsRegisteredWithPage) {
+                            /*StringBuffer coords = new StringBuffer(40);
+                            coords.append(model.getKeyAt(modelIndex)).append(SEP).
+                                    append(i);
+                            s.setControlEvent(getControler(), CELL_EVENT,
+                                              coords.toString());*/
+
+                            s.setControlEvent(getControler(),
+                                              CELL_EVENT,
+                                              String.format("%s%s%d",
+                                                            model.getKeyAt(
+                                    modelIndex),
+                                                            SEP,
+                                                            i));
+                        }
+
+                        Element cell = trow.newChildElement(BEBOP_CELL,
+                                                            BEBOP_XML_NS);
+
+                        tc.exportCellAttributes(cell);
+                        long begin = System.currentTimeMillis();
+                        r.getComponent(this, s, value, selected, key, row, i).
+                                generateXML(s, cell);
+                        logger.debug(String.format("until here i needed %d ms",
+                                                   System.currentTimeMillis()
+                                                   - begin));
+                    }
+                }
+                row += 1;
+                logger.debug(
+                        String.format("Created row in %d ms",
+                                      System.currentTimeMillis() - rowStart));
+            } while (model.nextRow());
+            logger.debug(String.format("Build table rows in %d ms",
+                                       System.currentTimeMillis() - start));
+        } else if (m_emptyView != null) {
+            m_emptyView.generateXML(s, p);
+        }
+        if (tableIsRegisteredWithPage) {
+            s.clearControlEvent();
+        }
+    }
+
+    protected Component getControler() {
+        return this;
+    }
+
+    /**
+     * Returns the table model in effect for the request represented by the
+     * page state.
+     *
+     * @param s represents the state of the page in the current request
+     * @return the table model used for outputting the table.
+     */
+    public TableModel getTableModel(PageState s) {
+        return (TableModel) m_tableModel.get(s);
+    }
+
+    /**
+     * Initialize the request local m_tableModel field so that
+     * it is initialized with whatever model the table model builder returns
+     * for the request.
+     */
+    private void initTableModel() {
+        m_tableModel = new RequestLocal() {
+
+            @Override
+            protected Object initialValue(PageState s) {
+                return m_modelBuilder.makeModel(Table.this, s);
+            }
+        };
+    }
+
+    /**
+     * Locks the table against further modifications. This also locks all
+     * the associated objects: the model builder, the column model, and the
+     * header components.
+     * @see com.arsdigita.util.Lockable#lock
+     */
+    @Override
+    public void lock() {
+        getModelBuilder().lock();
+        getColumnModel().lock();
+        if (getHeader() != null) {
+            getHeader().lock();
+        }
+        super.lock();
+    }
+
+    /**
+     * An internal class that creates a table model around a set of data given
+     * as a Object[][]. The table models produced by this builder
+     * use row numbers, converted to strings, as the key for each column of a 
+     * row.
+     */
+    public static class MatrixTableModelBuilder
+                        extends AbstractTableModelBuilder {
+
+        private final Object[][] m_data;
+
+        /**
+         * Constructor.
+         * 
+         * @param data 
+         */
+        public MatrixTableModelBuilder(Object[][] data) {
+            m_data = data;
+        }
+
+        @Override
+        public TableModel makeModel(Table t, PageState s) {
+            return new TableModel() {
+
+                private int row = -1;
+
+                @Override
+                public int getColumnCount() {
+                    return m_data[0].length;
+                }
+
+                @Override
+                public boolean nextRow() {
+                    return (++row < m_data.length);
+                }
+
+                @Override
+                public Object getElementAt(int j) {
+                    return m_data[row][j];
+                }
+
+                @Override
+                public Object getKeyAt(int j) {
+                    return String.valueOf(row);
+                }
+            };
+        }
+    }
+
+    /**
+     * A {@link TableModel} that has no rows.
+     */
+    public static final TableModel EMPTY_MODEL = new TableModel() {
+
+        @Override
+        public int getColumnCount() {
+            return 0;
+        }
+
+        @Override
+        public boolean nextRow() {
+            return false;
+        }
+
+        @Override
+        public Object getKeyAt(int column) {
+            throw new IllegalStateException("TableModel is empty");
+        }
+
+        @Override
+        public Object getElementAt(int column) {
+            throw new IllegalStateException("TableModel is empty");
+        }
+    };
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Tree.java b/ccm-core/src/main/java/com/arsdigita/bebop/Tree.java
new file mode 100644
index 000000000..d335bb90d
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Tree.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import static com.arsdigita.bebop.Component.*;
+
+import com.arsdigita.bebop.event.ActionEvent;
+import com.arsdigita.bebop.event.ActionListener;
+import com.arsdigita.bebop.event.ChangeEvent;
+import com.arsdigita.bebop.event.ChangeListener;
+import com.arsdigita.bebop.event.EventListenerList;
+import com.arsdigita.bebop.event.TreeExpansionEvent;
+import com.arsdigita.bebop.event.TreeExpansionListener;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.parameters.StringParameter;
+import com.arsdigita.bebop.tree.DefaultTreeCellRenderer;
+import com.arsdigita.bebop.tree.TreeCellRenderer;
+import com.arsdigita.bebop.tree.TreeModel;
+import com.arsdigita.bebop.tree.TreeModelBuilder;
+import com.arsdigita.bebop.tree.TreeNode;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.LockableImpl;
+import com.arsdigita.xml.Element;
+
+import java.util.Iterator;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Used to print a tree structure. Nodes can be in expanded or
+ * collapsed state.  Tree uses the getChildren() and
+ * getRoot() methods from TreeModel and traverses the iterator to get
+ * to all the nodes.
+ *
+ * This class keeps track of which nodes are expanded and collapsed
+ * and the hierarchy of nodes, and displays the tree correspondingly.
+ *
+ * @author David Lutterkort
+ * @author Stanislav Freidin
+ * @author Tri Tran
+ * @version $Id$
+ */
+public class Tree extends SimpleComponent implements Resettable {
+
+    private static final Logger s_log =
+        Logger.getLogger(Tree.class);
+
+    private static final boolean s_selectAttributeEnabled =
+        Bebop.getConfig().treeSelectAttributeEnabled();
+
+    // Any node id in the currentState is equivalent
+    // to that node being expanded.  If node id is
+    // NOT in the currentState, then it's collapsed.
+    private static final String CURRENT_STATE = "state";
+    private static final String EXPAND_EVENT = "expand";
+    private static final String COLLAPSE_EVENT = "collapse";
+    private static final String SELECT = "sel";
+    private static final String SELECT_EVENT = "s";
+
+    private static final boolean EXPANDED = true;
+    private static final boolean NOT_EXPANDED = false; // Collapsed
+    private static final boolean LEAF = true;
+    private static final boolean NOT_LEAF = false;
+
+    protected StringParameter m_currentState;
+
+    protected TreeModelBuilder m_builder;
+    private RequestLocal m_model;
+    private TreeModel m_tree;
+
+    private EventListenerList m_listeners;
+
+    private SingleSelectionModel m_selection;
+
+    private ChangeListener m_changeListener;
+
+    private Element treeElement;
+
+    private TreeCellRenderer m_renderer;
+
+    /**
+     * Constructs a new Tree using the specified
+     * {@link TreeModelBuilder}. The {@link TreeModelBuilder} will
+     * instantiate a {@link TreeModel} during each request.
+     *
+     * @param b the {@link TreeModelBuilder}
+     */
+    public Tree(TreeModelBuilder b) {
+        super();
+        m_currentState = new StringParameter(CURRENT_STATE);
+        m_builder = b;
+        m_renderer = new DefaultTreeCellRenderer();
+        m_selection = new ParameterSingleSelectionModel(new StringParameter(SELECT));
+        m_listeners = new EventListenerList();
+
+        m_model = new RequestLocal() {
+                protected Object initialValue(PageState s) {
+                    return getModelBuilder().makeModel(Tree.this, s);
+                }
+            };
+
+        m_tree = null;
+    }
+
+    /**
+     * Deprecated constructor that takes a default {@link TreeModel}
+     * and wraps it in a dummy TreeModelBuilder.
+     *
+     * @param t the TreeModel
+     * @deprecated This constructor has been deprecated in favor of
+     *   Tree(TreeModelBuilder b). It is not practical
+     *   to hardwire the TreeModel into the Tree,
+     *   since the model may change during each request. It is possible
+     *   to write the model-instantiation code in
+     *   {@link TreeModel#getRoot(PageState)}, but the
+     *   {@link TreeModelBuilder} fits better into the pattern which has
+     *   already been established by {@link List} and {@link Table}
+     */
+    public Tree(TreeModel t) {
+        this(new WrapperModelBuilder());
+        m_tree = t;
+    }
+
+    /**
+     * Registers the two parameters to the page.
+     */
+    public void register(Page p) {
+        Assert.isUnlocked(this);
+
+        p.addComponent(this);
+        p.addComponentStateParam(this, m_currentState);
+        p.addComponentStateParam(this, getSelectionModel().getStateParameter());
+    }
+
+    /**
+     * Clears the request state of the tree.
+     */
+    public void reset(final PageState state) {
+        clearSelection(state);
+        clearExpansionState(state);
+    }
+
+    /**
+     * Returns the tree model used for this tree.
+     *
+     * @return a TreeModel.
+     * @see #setTreeModel setTreeModel
+     * @see TreeModel
+     * @deprecated Use {@link #getTreeModel(PageState)} instead
+     */
+    public final TreeModel getTreeModel() {
+        return m_tree;
+    }
+
+    /**
+     * Returns the {@link TreeModel} used by the tree for the current
+     * request.
+     *
+     * @param s the page state
+     */
+    public TreeModel getTreeModel(PageState s) {
+        return (TreeModel)m_model.get(s);
+    }
+
+    /**
+     * @return the {@link TreeModelBuilder} used to build the tree model
+     *    for this tree.
+     */
+    public final TreeModelBuilder getModelBuilder() {
+        return m_builder;
+    }
+
+    /**
+     * @param b the new {@link TreeModelBuilder} for the tree
+     */
+    public void setModelBuilder(TreeModelBuilder b) {
+        Assert.isUnlocked(this);
+        m_builder = b;
+    }
+
+    /**
+     * Sets the tree model used for this tree.
+     *
+     * @return a TreeModel.
+     * @see #setTreeModel setTreeModel
+     * @see TreeModel
+     */
+    public void setTreeModel(TreeModel m) {
+        Assert.isUnlocked(this);
+        m_tree = m;
+    }
+
+    /**
+     * Sets the selection model, which keeps track of which node is
+     * currently selected. It can be used to manipulate the selection
+     * programmatically.
+     *
+     * @param m the new selection model
+     */
+    public void setSelectionModel(SingleSelectionModel m) {
+        Assert.isUnlocked(this);
+        m_selection = m;
+        s_log.debug("New model: " + m);
+    }
+
+    /**
+     * Gets the selection model, which keeps track of which node is
+     * currently selected. It can be used to manipulate the selection
+     * programmatically.
+     *
+     * @return the model used by the tree to keep track of the selected node.
+     */
+    public final SingleSelectionModel getSelectionModel() {
+        return m_selection;
+    }
+
+    /**
+     * Gets the key for the selected node. This will only be a valid key
+     * if {@link #isSelected isSelected} is true.
+     *
+     * @param state represents the state of the current request
+     * @return the key for the selected node.
+     * @pre isSelected(state)
+     */
+    public Object getSelectedKey(PageState state) {
+        return m_selection.getSelectedKey(state);
+    }
+
+    /**
+     * Sets the selection to the one with the specified key. If
+     * key was not selected already, fires the {@link
+     * ChangeEvent}.
+     *
+     * @param state represents the state of the current request
+     * @param key the key for the selected node
+     * @see #fireStateChanged fireStateChanged
+     */
+    public void setSelectedKey(PageState state, Object key) {
+        m_selection.setSelectedKey(state, key);
+    }
+
+    /**
+     * Returns true if one of the nodes is currently selected.
+     *
+     * @param state represents the state of the current request
+     * @return true if one of the nodes is selected;
+     * false otherwise.
+     */
+    public boolean isSelected(PageState state) {
+        return m_selection.isSelected(state);
+    }
+
+    /**
+     * Clears the selection in the request represented by state.
+     *
+     * @param state represents the state of the current request
+     * @post ! isSelected(state)
+     */
+    public void clearSelection(PageState state) {
+        m_selection.clearSelection(state);
+    }
+
+    /**
+     * Tells whether the tree has state on the request for tree node
+     * expansion.
+     */
+    public final boolean hasExpansionState(final PageState state) {
+        return state.getValue(m_currentState) != null;
+    }
+
+    /**
+     * Clears any tree node expansion state on the request.
+     */
+    public final void clearExpansionState(final PageState state) {
+        state.setValue(m_currentState, null);
+    }
+
+    /**
+     * Creates the change listener used for forwarding change events fired by
+     * the selection model to change listeners registered with the tree. The
+     * returned change listener refires the event with the tree,
+     * rather than the selection model, as source.
+     *
+     * @return the change listener used internally by the tree.
+     */
+    protected ChangeListener createChangeListener() {
+        return new ChangeListener() {
+                public void stateChanged(ChangeEvent e) {
+                    fireStateChanged(e.getPageState());
+                }
+            };
+    }
+
+    /**
+     * Adds a change listener. A change event is fired whenever the selected
+     * tree node changes during the processing of a request. The change event
+     * that listeners receive names the tree as the source.
+     *
+     * @param l the change listener to run when the selected item changes in
+     * a request
+     * @pre ! isLocked()
+     */
+    public void addChangeListener(ChangeListener l) {
+        Assert.isUnlocked(this);
+        if ( m_changeListener == null ) {
+            m_changeListener = createChangeListener();
+
+            if (s_log.isDebugEnabled()) {
+                s_log.debug("Adding listener " + l + " to " + this);
+            }
+
+            m_selection.addChangeListener(m_changeListener);
+        }
+        m_listeners.add(ChangeListener.class, l);
+    }
+
+    /**
+     * Removes a change listener. The listener should have been previously
+     * added with {@link #addChangeListener addChangeListener}, although no
+     * error is signalled if the change listener is not found among the
+     * tree's listeners.
+     *
+     * @param l the change listener to remove from the tree
+     */
+    public void removeChangeListener(ChangeListener l) {
+        Assert.isUnlocked(this);
+
+        if (s_log.isDebugEnabled()) {
+            s_log.debug("Removing listener " + l + " from " + this);
+        }
+
+        m_listeners.remove(ChangeListener.class, l);
+    }
+
+    /**
+     * Fires a change event to signal that the selected list item has changed
+     * in the request represented by state. The source of the
+     * event is the tree.
+     *
+     * @param state represents the state of the current request
+     */
+    protected void fireStateChanged(PageState state) {
+        Iterator
+            i=m_listeners.getListenerIterator(ChangeListener.class);
+        ChangeEvent e = null;
+
+        while (i.hasNext()) {
+            if ( e == null ) {
+                e = new ChangeEvent(this, state);
+            }
+            ((ChangeListener) i.next()).stateChanged(e);
+        }
+    }
+
+    /**
+     * Adds a listener that is notified whenever a user clicks on any part
+     * of the tree, either to expand or collapse a node, or to select a
+     * node. The listener is run whenever {@link #respond respond} is
+     * called.
+     *
+     * @pre l != null
+     * @pre ! isLocked()
+     */
+    public void addActionListener(ActionListener l) {
+        Assert.isUnlocked(this);
+        m_listeners.add(ActionListener.class, l);
+    }
+
+    /**
+     * Removes a previously added ActionListener.
+     * @see #addActionListener addActionListener
+     */
+    public void removeActionListener(ActionListener l) {
+        Assert.isUnlocked(this);
+        m_listeners.remove(ActionListener.class, l);
+    }
+
+    /**
+     * Notifies listeners that some part of the tree was clicked by the
+     * user. The source of the event is the tree.
+     *
+     * @pre data != null
+     * @see #respond respond
+     */
+    protected void fireActionEvent(PageState data) {
+        Iterator
+            i=m_listeners.getListenerIterator(ActionListener.class);
+        ActionEvent e = null;
+
+        while (i.hasNext()) {
+            if ( e == null ) {
+                e = new ActionEvent(this, data);
+            }
+            ((ActionListener) i.next()).actionPerformed(e);
+        }
+    }
+
+    /**
+     * Adds a listener that is notified whenever a tree node is expanded or
+     * collpased, either by a user's click or by explicit calls to {@link
+     * #expand expand} or {@link #collapse collapse}.
+     *
+     * @pre l != null
+     * @pre ! isLocked()
+     */
+    public void addTreeExpansionListener(TreeExpansionListener l) {
+        Assert.isUnlocked(this);
+        m_listeners.add(TreeExpansionListener.class, l);
+    }
+
+    /**
+     * Removes a previously added TreeExpansionListener.
+     *
+     * @pre ! isLocked()
+     * @see #addTreeExpansionListener addTreeExpansionListener
+     */
+    public void removeTreeExpansionListener(TreeExpansionListener l) {
+        Assert.isUnlocked(this);
+        m_listeners.remove(TreeExpansionListener.class, l);
+    }
+
+    /**
+     * Notifies all registered {@link
+     * com.arsdigita.bebop.event.TreeExpansionListener
+     * TreeExpansionListeners} that a node in the tree has been expanded.
+     *
+     * @pre state != null
+     * @pre nodeKey != null
+     */
+    protected void fireTreeExpanded(PageState state, Object nodeKey) {
+        Iterator i =
+            m_listeners.getListenerIterator(TreeExpansionListener.class);
+        TreeExpansionEvent e = null;
+
+        while (i.hasNext()) {
+            if ( e == null ) {
+                e = new TreeExpansionEvent(this, state, nodeKey);
+            }
+            ((TreeExpansionListener) i.next()).treeExpanded(e);
+        }
+    }
+
+    /**
+     * Notifies all registered {@link
+     * com.arsdigita.bebop.event.TreeExpansionListener
+     * TreeExpansionListeners} that a node in the tree has been collapsed.
+     *
+     * @pre state != null
+     * @pre nodeKey != null
+     */
+    protected void fireTreeCollapsed(PageState state, Object nodeKey) {
+        Iterator i =
+            m_listeners.getListenerIterator(TreeExpansionListener.class);
+        TreeExpansionEvent e = null;
+
+        while (i.hasNext()) {
+            if ( e == null ) {
+                e = new TreeExpansionEvent(this, state, nodeKey);
+            }
+            ((TreeExpansionListener) i.next()).treeCollapsed(e);
+        }
+    }
+
+    /**
+     * Notifies the Tree that a node has been selected.
+     * Changes the currently selected tree component.
+     */
+    public void respond(PageState data) throws javax.servlet.ServletException {
+        String action = data.getControlEventName();
+        String node = data.getControlEventValue();
+
+        if (EXPAND_EVENT.equals(action)) {
+            expand(node, data);
+        } else if (COLLAPSE_EVENT.equals(action)) {
+            collapse(node, data);
+        } else if ( SELECT_EVENT.equals(action) ) {
+            setSelectedKey(data, data.getControlEventValue());
+        } else {
+            throw new javax.servlet.ServletException("Unknown event '" + action + "'");
+        }
+        fireActionEvent(data);
+    }
+
+    //////////////////////////////
+    // MANAGE TREE'S NODE STATE //
+    //////////////////////////////
+
+    /**
+     * Determines whether the node at the specified display row is collapsed.
+     * @return true if the node at the specified display row is collapsed;
+     * false otherwise.
+     */
+    public boolean isCollapsed(String nodeKey, PageState data) {
+        String stateString = (String) data.getValue(m_currentState);
+        String spaceId = " " + nodeKey + " ";
+        int idIndex;
+
+        if (stateString == null) {
+            return true;
+        } else {
+            idIndex = stateString.indexOf(spaceId);
+        }
+
+        // == -1 means it's not found in current state, thus it's collapsed
+        return (idIndex == -1);
+    }
+
+    /**
+     * Collapses a node in the tree and makes its children visible.
+     *
+     * @param nodeKey the key that the tree model uses to identify the
+     * node
+     * @param data represents the current request
+     *
+     * @pre nodeKey != null
+     * @pre data != null
+     */
+    public void collapse(String nodeKey, PageState data) {
+        Assert.exists(nodeKey);
+        Assert.exists(data);
+
+        StringBuffer newCurrentState = new StringBuffer("");
+        String stateString = (String) data.getValue(m_currentState);
+        int idIndex;
+        String spaceId = " " + nodeKey + " ";
+        int idLength = spaceId.length();
+
+        if (stateString != null) {
+            idIndex = stateString.indexOf(spaceId);
+            // Found it; it should currently be expanded, so collapse it
+            if (idIndex != -1) {
+                newCurrentState
+                    .append(stateString.substring(0,idIndex))
+                    .append(" ");
+                if (stateString.length() > (idIndex + idLength)) {
+                    newCurrentState.append(stateString.substring(idIndex+idLength));
+                }
+                data.setValue(m_currentState, newCurrentState.toString());
+                fireTreeCollapsed(data, nodeKey);
+            }
+        }
+    }
+
+    /**
+     * Expands a node in the tree and makes its children visible.
+     *
+     * @param nodeKey the key that the tree model uses to identify the
+     * node
+     * @param data represents the current request
+     *
+     * @pre nodeKey != null
+     * @pre data != null
+     */
+    public void expand(String nodeKey, PageState data) {
+        Assert.exists(nodeKey);
+        Assert.exists(data);
+
+        String stateString = (String) data.getValue(m_currentState);
+        String spaceId = " " + nodeKey + " ";
+        StringBuffer newCurrentState = new StringBuffer("");
+
+        if (stateString != null) {
+            // Can't find it; it should currently be collapsed, so expand it
+            if ((stateString.indexOf(spaceId)) == -1) {
+                // Start with existing stateString...
+                newCurrentState.append(stateString);
+                // Add to newCurrentState string the new node Id
+                newCurrentState.append(spaceId);
+                // Set the value of the current state
+                data.setValue(m_currentState, newCurrentState.toString());
+                fireTreeExpanded(data, nodeKey);
+            }
+        } else {
+            // Add to newCurrentState string the new node Id
+            newCurrentState.append(spaceId);
+            // Set the value of the current state
+            data.setValue(m_currentState, newCurrentState.toString());
+            fireTreeExpanded(data, nodeKey);
+        }
+    }
+
+    /////////////////////////////////////////////
+    // PRINT THE TREE DIRECTLY OR GENERATE DOM //
+    /////////////////////////////////////////////
+
+    /**
+     * Returns the renderer currently used to render tree nodes.
+     *
+     * @return the current tree node renderer.
+     */
+    public final TreeCellRenderer getCellRenderer() {
+        return m_renderer;
+    }
+
+    /**
+     * Sets the cell renderer to be used when generating output with
+     * {@link #generateXML generateXML}.
+     *
+     * @param r a TreeCellRenderer value
+     */
+    public void setCellRenderer(TreeCellRenderer r) {
+        Assert.isUnlocked(this);
+        m_renderer = r;
+    }
+
+    private boolean hasSelectedChild(TreeModel tree, TreeNode node, PageState data, Object selKey) {
+        String nodeKey = (String) node.getKey();
+        if ( (selKey != null) && (selKey.equals(nodeKey) || selKey.toString().equals(nodeKey)) ) {
+            return true;
+        }
+        Iterator i = tree.getChildren(node, data);
+        while (i.hasNext()) {
+            TreeNode child = (TreeNode) i.next();
+            if (hasSelectedChild(tree, child, data, selKey)) {
+                // At this point we should close the opened DataQuery pointed to by Iterator (i).
+                // Since the data query is wrapped within DataQueryIterator, we don't have
+                // access to it directly, so this looks like the only viable option ...
+                while (i.hasNext()) { }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Builds a DOM representing the tree.
+     *
+     */
+    protected void generateTree(PageState data, Element parent, TreeNode node,
+            TreeModel tree) {
+
+        Element t_node = parent.newChildElement ("bebop:t_node", BEBOP_XML_NS);
+
+        String nodeKey = (String) node.getKey();
+        Object selKey = getSelectedKey(data);
+        boolean isSelected = (selKey != null)
+            && (selKey.equals(nodeKey) || selKey.toString().equals(nodeKey));
+
+        boolean hasChildren = tree.hasChildren(node, data);
+        if (s_selectAttributeEnabled) {
+            boolean hasSelectedChild = false;
+            if (!isSelected && hasChildren) {
+                hasSelectedChild = hasSelectedChild(tree, node, data, selKey);
+            }
+            t_node.addAttribute("isSelected", String.valueOf(isSelected | hasSelectedChild));
+        }
+
+        if (hasChildren) {
+            boolean collapsed = isCollapsed(nodeKey,data);
+            data.setControlEvent(this, collapsed ? EXPAND_EVENT : COLLAPSE_EVENT, nodeKey);
+            try {
+                t_node.addAttribute("href", data.stateAsURL());
+            } catch (java.io.IOException ioe) {
+                // TODO: stateAsURL failed
+            }
+            data.clearControlEvent();
+            if ( collapsed ) {
+                // Collapsed
+                t_node.addAttribute("collapsed", "t");
+                data.setControlEvent(this, SELECT_EVENT, nodeKey);
+                Component c = getCellRenderer().getComponent(this, data,
+                        node.getElement(), isSelected, NOT_EXPANDED, NOT_LEAF,
+                        nodeKey);
+                c.generateXML(data, t_node);
+            } else {
+                // Expanded
+                t_node.addAttribute("expanded", "t");
+                data.setControlEvent(this, SELECT_EVENT, nodeKey);
+                Component c = getCellRenderer().getComponent(this, data,
+                        node.getElement(), isSelected, EXPANDED, NOT_LEAF,
+                        nodeKey);
+                c.generateXML(data, t_node);
+                t_node.addAttribute("indentStart", "t");
+                for(Iterator i = tree.getChildren(node,data); i.hasNext(); ) {
+                    generateTree(data, t_node, (TreeNode) i.next(), tree);
+                }
+                t_node.addAttribute("indentClose", "t");
+            }
+        } else {
+            // No children, no need for link...
+            t_node.addAttribute("childless", "t");
+            data.setControlEvent(this, SELECT_EVENT, nodeKey);
+            Component c = getCellRenderer().getComponent(this, data,
+                    node.getElement(), isSelected, NOT_EXPANDED, LEAF, nodeKey);
+            c.generateXML(data, t_node);
+        }
+    }
+
+    /**
+     * Services the request by building a DOM tree with the nodes
+     * first and then the included page.
+     */
+    public void generateXML(PageState data, Element parent) {
+
+        TreeModel tree = getTreeModel(data);
+
+        if ( ! isVisible(data) ) {
+            return;
+        }
+
+        treeElement = parent.newChildElement ("bebop:tree", BEBOP_XML_NS);
+        exportAttributes(treeElement);
+
+        TreeNode _rootNode = tree.getRoot(data);
+        generateTree(data, treeElement, _rootNode, tree);
+
+    }
+
+    /**
+     * Manage the selected item by manipulating the state parameter.
+     *
+     * @deprecated The {@link ParameterSingleSelectionModel} contains
+     * all the functionality of this class
+     */
+    public static class TreeSingleSelectionModel
+        extends ParameterSingleSelectionModel {
+        public TreeSingleSelectionModel(ParameterModel m) {
+            super(m);
+        }
+    }
+
+    /**
+     * Locks the Tree and prohibits further modifications.
+     */
+    public void lock() {
+        getModelBuilder().lock();
+        super.lock();
+    }
+
+    /**
+     * Returns the tree model of the tree. A wrapper class to make
+     * deprecated constructor work.
+     */
+    private static class WrapperModelBuilder extends LockableImpl
+        implements TreeModelBuilder {
+
+        public WrapperModelBuilder() {
+            super();
+        }
+
+        public TreeModel makeModel(Tree t, PageState s) {
+            return t.getTreeModel();
+        }
+    }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/AbstractTableModelBuilder.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/AbstractTableModelBuilder.java
new file mode 100644
index 000000000..34c282984
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/AbstractTableModelBuilder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.table;
+
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.Table;
+import com.arsdigita.util.LockableImpl;
+
+/**
+ * A convenience for implementing TableModelBuilders. This
+ * class provides a default implementation of the methods demanded by
+ * Lockable, so that implementors of
+ * TableModelBuilder only need to override the
+ * makeModel method.
+ *
+ * @author David Lutterkort
+ * @see TableModelBuilder
+ * @see com.arsdigita.util.Lockable
+ *
+ * @version $Id$
+ */
+public abstract class AbstractTableModelBuilder extends LockableImpl
+    implements TableModelBuilder {
+
+    /**
+     * Return a table model for the request represented by
+     * s. The table model contains all the data that is to be
+     * displayed in a table. The returned table model is used only during
+     * the duration of that request.
+     *
+     * @param t the table which will use this table model
+     * @param s represents the current request
+     * @return the data to be displayed in the table
+     */
+    public abstract TableModel makeModel(Table t, PageState s);
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableCellRenderer.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableCellRenderer.java
new file mode 100644
index 000000000..d84c63a29
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableCellRenderer.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.table;
+
+
+import com.arsdigita.bebop.util.GlobalizationUtil ; 
+
+import com.arsdigita.bebop.Component;
+import com.arsdigita.bebop.ControlLink;
+import com.arsdigita.bebop.Label;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.Table;
+import com.arsdigita.globalization.GlobalizedMessage;
+
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.LockableImpl;
+
+/**
+ * The default renderer for table cells. This renderer is used by the
+ * {@link com.arsdigita.bebop.Table} component for rendering the table
+ * headers and cells if no other renderer is specified.
+ *
+ * 

This renderer can operate in two different modes: active + * and inactive mode. In inactive mode, all objects are rendered + * by converting them to a string and enclosing that string in a {@link + * com.arsdigita.bebop.Label}. If the renderer is in active mode, this + * label is further enclosed in a control link. When the user clicks on + * this link, the table will fire an TableActionEvent whose + * getKey() and getColumn() method return the + * values of the key and column parameters that + * were passed into {@link #getComponent getComponent}. + * + *

In a nutshell, an active renderer will let the user click a link + * that causes a TableActionEvent for the corresponding cell, + * while an inactive renderer will display the values just as strings, thus + * making it impossible for the user to cause such an event. + * + * @author David Lutterkort + * @see com.arsdigita.bebop.Table + * @see com.arsdigita.bebop.event.TableActionEvent + * + * @version $Id$ */ +public class DefaultTableCellRenderer extends LockableImpl + implements TableCellRenderer { + + private boolean m_active; + private ThreadLocal m_label; + private ThreadLocal m_controlLink; + + /** + * Creates a new table cell renderer. The table cell renderer is in + * inactive mode. + */ + public DefaultTableCellRenderer() { + this(false); + } + + /** + * Creates a new table cell renderer. The active argument + * specifies whether the renderer should be active or not. + * + * @param active true if the renderer should generate links + * instead of just static labels. + */ + public DefaultTableCellRenderer(boolean active) { + m_active = active; + m_label = new ThreadLocal() { + protected Object initialValue() { + return new Label(""); + } + }; + m_controlLink = new ThreadLocal() { + protected Object initialValue() { + return new ControlLink((Label) m_label.get()); + } + }; + } + + /** + * Return true if the renderer is in active mode. A + * rendererin active mode will enclose the objects it renders in links + * that, when clicked, will cause the containing table to fire a + * TableActionEvent. + * + * @return true if the renderer is in active mode. + */ + public final boolean isActive() { + return m_active; + } + + /** + * Set the renderer to active or inactive mode. + * + * @param v true if the renderer should operate in active + * mode. + * @pre ! isLocked() + */ + public void setActive(boolean v) { + Assert.isUnlocked(this); + m_active = v; + } + + /** + * Return the component that should be used to render the given + * value. Returns a {@link com.arsdigita.bebop.Label} if the + * renderer is active, and a {@link com.arsdigita.bebop.ControlLink} if + * the renderer is inactive. + * + * @pre table == null || table != null + */ + public Component getComponent(Table table, PageState state, Object value, + boolean isSelected, Object key, + int row, int column) + { + if ( ! isLocked() && table != null && table.isLocked() ) { + lock(); + } + + Label l; + if ( value instanceof com.arsdigita.bebop.Component ) { + return (com.arsdigita.bebop.Component) value; + } else if(value instanceof GlobalizedMessage) { + l = (Label) m_label.get(); + l.setLabel((GlobalizedMessage) value); + } else { + l = (Label) m_label.get(); + + if ( value == null ) { + l.setLabel( (String) GlobalizationUtil.globalize("bebop.table.").localize()); + l.setOutputEscaping(false); + } else { + l.setLabel(value.toString()); + l.setOutputEscaping(true); + } + } + l.setFontWeight( (isSelected && m_active) ? Label.BOLD : null ); + if (m_active && ! isSelected) { + return (ControlLink) m_controlLink.get(); + } else { + return l; + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableColumnModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableColumnModel.java new file mode 100644 index 000000000..63e25c810 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/DefaultTableColumnModel.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.table; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.arsdigita.bebop.SingleSelectionModel; +import com.arsdigita.bebop.ParameterSingleSelectionModel; +import com.arsdigita.bebop.parameters.IntegerParameter; +import com.arsdigita.util.Assert; + +/** + * Describe interface TableColumnModel here. + * + * @author David Lutterkort + * @version $Id$ + */ +public class DefaultTableColumnModel implements TableColumnModel { + + private static final String SELECTED_COLUMN="col"; + + private boolean m_locked; + + private ArrayList m_columns; + + private SingleSelectionModel m_selection; + + public DefaultTableColumnModel() { + this(new Object[0]); + } + + public DefaultTableColumnModel(SingleSelectionModel sel) { + this(new Object[0], sel); + } + + public DefaultTableColumnModel(Object[] headers) { + this(headers, + new ParameterSingleSelectionModel(new IntegerParameter(SELECTED_COLUMN)) ); + } + + public DefaultTableColumnModel(Object[] headers, SingleSelectionModel sel) { + m_columns = new ArrayList(); + m_selection = sel; + + for (int i=0; i < headers.length; i++) { + add(new TableColumn(i, headers[i], new Integer(i))); + } + } + + public void add(TableColumn column) { + Assert.isUnlocked(this); + m_columns.add(column); + } + + public void add(int columnIndex, TableColumn column) { + Assert.isUnlocked(this); + m_columns.add(columnIndex, column); + } + + public TableColumn get(int columnIndex) { + return (TableColumn) m_columns.get(columnIndex); + } + + public void set(int columnIndex, TableColumn v) { + m_columns.set(columnIndex, v); + } + + public int size() { + return m_columns.size(); + } + + public int getIndex(Object key) { + if ( key == null ) { + return -1; + } + for (int i=0; i The table uses the returned component only until it calls the cell + * renderer again, so that cell renderers may reuse the same object in + * subsequent calls to {@link #getComponent getComponent}. + * + *

As an example, consider the following implementation of a table cell + * renderer, which simply converts the passed in value to a + * string and encloses it in a label. The cell renderer converts the passed + * in value to a string and uses that to set the text to display for a + * label. If the value is selected, the label is bolded. As an added twist, + * the table cell renderer uses only one label for each thread from which + * it is accessed (rather than creating a new Label for each + * call) by storing the label in a ThreadLocal variable. + * + *

+ * public class MyTableCellRenderer implements TableCellRenderer {
+ *
+ *   private ThreadLocal m_label;
+ *
+ *   public MyTableCellRenderer() {
+ *     m_label = new ThreadLocal() {
+ *  protected Object initialValue() {
+ *    return new Label("");
+ *  }
+ *       };
+ *   }
+ *
+ *   public Component getComponent(Table table, PageState state, Object value,
+ *              boolean isSelected, Object key,
+ *              int row, int column) {
+ *     Label l = (Label) m_label.get();
+ *     l.setLabel(value.toString());
+ *     l.setFontWeight( isSelected ? Label.BOLD : null );
+ *     return l;
+ *   }
+ * }
+ * 
+ * + * @author David Lutterkort + * @see com.arsdigita.bebop.Table Table + * @version $Id$ + */ +public interface TableCellRenderer { + + /** + * Return a component with the visual representation for the passed in + * key and value. + * + *

The table sets the control event prior to calling this method, so + * that any control link returned as the component will, when clicked, + * cause the table to fire a TableActionEvent whose + * getRowKey() and getColumn() return the + * values of key and column. A simple cell + * renderer that achieves this would implement this method in the + * following way: + *

+     *   public Component getComponent(Table table, PageState state, Object value,
+     *              boolean isSelected, Object key,
+     *              int row, int column) {
+     *     return new ControlLink(value.toString());
+     *   }
+     * 
+ * + *

The column refers to a column in the table's {@link + * TableColumnModel}, i.e. the visual column on the screen, and not the + * table's representation of the underlying data in the {@link + * TableModel}. + * + * @param table the table requesting the rendering. + * @param state represents the state of the current request. + * @param value the data element to render as returned by the table + * model's {@link TableModel#getElementAt getElementAt(column)}. + * @param isSelected true if this item is selected. + * @param key the key identifying this row (and possibly column) as + * returned by the table model's {@link TableModel#getKeyAt + * getKeyAt(column)} + * @param row the number of the row in the table, the first row has + * number 0. + * @param column the number of the table column. + * @return the component that should be used to render the + * value. + * @pre table != null + * @pre state != null + * @pre value != null + * @pre key != null + * @pre row >= 0 + * @pre column >= 0 && column < table.getColumnModel().size() + * @post return != null + * @see TableColumnModel + */ + Component getComponent(Table table, PageState state, Object value, + boolean isSelected, Object key, + int row, int column); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumn.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumn.java new file mode 100644 index 000000000..38c941f69 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumn.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.table; + +import static com.arsdigita.bebop.Component.*; + +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.SimpleComponent; + +import com.arsdigita.util.Assert; +import com.arsdigita.bebop.util.Attributes; +import com.arsdigita.util.Lockable; +import com.arsdigita.xml.Element; + +/** + * One column in a table. The TableColumn stores important + * display-related information about a table column, such as the column + * header, the renderers for the column header and ordinary cells in this + * column and from which column in the table model values should be taken + * when rendering table cells. The set of table columns for a table is + * maintained by a {@link TableColumnModel}. + * + *

TableColumn allows the column ordering to be different + * between the underlying {@link TableModel} and the view presented by the + * Table: each column contains a modelIndex + * property. This is the column that is retrieved from the + * TableModel when the values are displayed, regardless of the + * position of the TableColumn in the + * TableColumnModel. This makes it possible to display the + * same table model in several tables with reordered or omitted columns. + * + *

The TableColumn stores also the value and key used for + * the header of the column. These objects are passed to the header cell + * renderer when the header of the table is rendered. The value is usually + * used to generate the visible information for the table header, and is + * often a string. The key is usually used to identify the underlying + * object, or to just identify the column, and can be any object whose + * toString() method returns a representation that can be + * included in a URL. In the simplest case, this may just be an + * Integer containing the index of the column in the column + * model. + * + * @author David Lutterkort + * @see com.arsdigita.bebop.Table + * @see TableColumnModel + * + * @version $Id$ */ +public class TableColumn extends SimpleComponent + implements Lockable { + + + /** + * The name of the width attribute used in the XML. + */ + private static final String WIDTH_ATTR = "width"; + + /** + * The name of the align attribute used in the XML. + */ + private static final String ALIGN_ATTR = "align"; + + /** + * The name of the valign attribute used in the XML. + */ + private static final String VALIGN_ATTR = "valign"; + + /** + * The number of the column in the table model from which to get values. + */ + private int m_modelIndex; + + /** + * The renderer used for ordinary cells in this column. Null by default, + * which instructs the Table to use its default renderer. + */ + private TableCellRenderer m_cellRenderer; + + /** + * The renderer used for the header of the column. Null by default, which + * instructs the TableHeader to use its default renderer. + */ + private TableCellRenderer m_headerRenderer; + + /** + * The key for identifying the header. Will be passed to the header cell + * renderer. + */ + private Object m_headerKey; + + /** + * The display value for identifying the header. Will be passed to the + * header cell renderer. + * Usually this will be a {@link Label} passed in by a pattern like + * {@code new Label(GlobalizedMessage)}. But it could be any object, + * e.g.an image as well. The use of a string is possible but strongly + * discouraged because it results in non-localizable UI. + */ + private Object m_headerValue; + + /** + * The display attributes for each cell in this column + */ + private Attributes m_cellAttrs; + + /** + * Creates a new table column with modelIndex 0 and header + * value and key equal to null. + */ + public TableColumn() { + this(0); + } + + /** + * Creates a new table column with the given modelIndex and + * header value and key equal to null. + * + * @param modelIndex the index of the column in the table model from + * which to retrieve values + * @pre modelIndex >= 0 + */ + public TableColumn(int modelIndex) { + this(modelIndex, null); + } + + /** + * Creates a new table column with the given modelIndex and + * header value. The header key is equal to null. + * + * @param modelIndex the index of the column in the table model from + * which to retrieve values. + * @param value the value for the column header. + * @pre modelIndex >= 0 + */ + public TableColumn(int modelIndex, Object value) { + this(modelIndex, value, null); + } + + /** + * Creates a new table column with the given modelIndex and + * header value and key. + * + * @param modelIndex the index of the column in the table model from + * which to retrieve values. + * @param value the value for the column header. + * @param key the key for the column header. + * @pre modelIndex >= 0 + */ + public TableColumn(int modelIndex, Object value, Object key) { + super(); + m_modelIndex = modelIndex; + m_headerValue = value; + m_headerKey = key; + + m_cellAttrs = new Attributes(); + } + + /** + * Return the renderer used for the column header. This is + * null by default, in which case the default renderer for + * the {@link TableHeader} of the table to which this column belongs is + * used. + * + * @return the renderer used for the column header. + */ + public final TableCellRenderer getHeaderRenderer() { + return m_headerRenderer; + } + + /** + * Set the renderer used for the column header. The header key and value + * objects are passed to the renderer when the column header will be + * rendererd. + * + * @param v the new renderer for the column header. + * @see #getHeaderRenderer + * @see #getCellRenderer + */ + public void setHeaderRenderer(TableCellRenderer v) { + Assert.isUnlocked(this); + m_headerRenderer = v; + } + + /** + * Return the renderer used for the cells in this column. This is + * null by default, in which case the default renderer of + * the {@link com.arsdigita.bebop.Table#getDefaultCellRenderer() table} to which this column + * belongs is used. + * + * @return the renderer used for the cells in this column. + */ + public final TableCellRenderer getCellRenderer() { + return m_cellRenderer; + } + + /** + * Set the renderer used for cells in this column. + * + * @param v the new renderer for the cells in this column. + * @see #getCellRenderer + * @see #getHeaderRenderer + */ + public void setCellRenderer(TableCellRenderer v) { + Assert.isUnlocked(this); + m_cellRenderer = v; + } + + /** + * Get the display value used for the header. This is the object that is + * passed to the renderer. Usually this will be a {@link Label} previously + * passed in by a pattern like {@code new Label(GlobalizedMessage)}. + * The use of a string is possible but strongly discouraged. + * + * @return the display value for the header. + */ + public final Object getHeaderValue() { + return m_headerValue; + } + + /** + * Set the display value for the header. This object is passed through to + * the header renderer without any modifications. + * Usually this will be a {@link Label} passed in by a pattern like + * {@code new Label(GlobalizedMessage)}. The use of a string is possible + * but strongly discouraged because it results in non-localizable UI. + * + * @param value the new display value for the header. + * @see #getHeaderValue + */ + public void setHeaderValue(Object value) { + Assert.isUnlocked(this); + m_headerValue = value; + } + + /** + * Get the key used to identify the header of this column. In the + * simplest case, this is an Integer containing the index of + * the column. + * + * @return the key used to identify the header of this column. + */ + public final Object getHeaderKey() { + return m_headerKey; + } + + /** + * Set the key used to identify the header of this column. + * + * @param key the new key for identifying the header of this column. + * @see #getHeaderKey + */ + public void setHeaderKey(Object key) { + Assert.isUnlocked(this); + m_headerKey = key; + } + + /** + * Get the index of the column from which values are taken in the {@link + * TableModel}. + * + * @return the index of the column in the table model from which values + * are taken. + * @see #setModelIndex setModelIndex + */ + public final int getModelIndex() { + return m_modelIndex; + } + + /** + * Set the index of the column in the {@link TableModel} from which the + * values are taken when this column is rendered. + * + * @param v the new index of the column in the table model from which to + * take values. + */ + public void setModelIndex(int v) { + Assert.isUnlocked(this); + m_modelIndex = v; + } + + /** + * Get the width for this column. + * + * @return the width of this column. + * @see #setWidth setWidth + */ + public String getWidth() { + return getAttribute(WIDTH_ATTR); + } + + /** + * Set the width of this column. The string v is added as an + * attribute to the XML element for this column in the table header. + * + * @param v the width of this column + */ + public void setWidth(String v) { + Assert.isUnlocked(this); + setAttribute(WIDTH_ATTR, v); + } + + /** + * Set the horizontal alignment this column. The string v + * is added as an attribute to the XML element for each cell in this column + * + * @param v the width of this column + */ + public void setAlign(String v) { + Assert.isUnlocked(this); + m_cellAttrs.setAttribute(ALIGN_ATTR, v); + } + + + /** + * Set the horizontal alignment this column's header. The string + * v is added as an attribute to the XML element for + * the column's header cell. + * + * @param v the width of this column */ + public void setHeadAlign(String v) { + Assert.isUnlocked(this); + setAttribute(ALIGN_ATTR, v); + } + + /** + * Set the vertical alignment this column. The string v + * is added as an attribute to the XML element for each cell in this column + * + * @param v the width of this column + */ + public void setVAlign(String v) { + Assert.isUnlocked(this); + m_cellAttrs.setAttribute(VALIGN_ATTR, v); + } + + /** + * Set the vertical alignment this column's header. The string + * v is added as an attribute to the XML element for + * this column's header cell. + * + * @param v the width of this column */ + public void setHeadVAlign(String v) { + Assert.isUnlocked(this); + setAttribute(VALIGN_ATTR, v); + } + + + /** + * Sets the style attribute for the column's + * cells. style should be a valid CSS style, since + * its value will be copied verbatim to the output and appear as a + * style attribute in the top level XML or HTML output + * element. + * + * @param style a valid CSS style description for use in the + * style attribute of an HTML tag + * @see Standard Attributes */ + public void setStyleAttr(String style) { + Assert.isUnlocked(this); + m_cellAttrs.setAttribute(STYLE, style); + } + + /** + * Sets the style attribute for the column's header + * cell. style should be a valid CSS style, since its + * value will be copied verbatim to the output and appear as a + * style attribute in the top level XML or HTML output + * element. + * + * @param style a valid CSS style description for use in the + * style attribute of an HTML tag + * @see Standard Attributes */ + public void setHeadStyleAttr(String style) { + Assert.isUnlocked(this); + setAttribute(STYLE, style); + } + + /** + * Sets the class attribute for the column's + * cells. style should be the name of a defined CSS + * class, since its value will be copied verbatim to the output + * and appear as a class attribute in the top level XML + * or HTML output element. + * + * @param style a valid CSS style description for use in the + * style attribute of an HTML tag + * @see Standard Attributes */ + public void setClassAttr(String c) { + Assert.isUnlocked(this); + m_cellAttrs.setAttribute(CLASS, c); + } + + + /** + * Sets the class attribute for the column's header + * cell. style should be the name of a defined CSS + * class, since its value will be copied verbatim to the output + * and appear as a class attribute in the top level XML + * or HTML output element. + * + * @param style a valid CSS style description for use in the + * style attribute of an HTML tag + * @see Standard Attributes */ + public void setHeadClassAttr(String c) { + Assert.isUnlocked(this); + setAttribute(CLASS, c); + } + + + + /** + * Add all the XML attributes for this column. + * + * @param e the XML element to which attributes will be added. + */ + public void exportCellAttributes(Element e) { + m_cellAttrs.exportAttributes(e); + } + + /** + * Add all the XML attributes for this column to this + * element. Package-friendly since it is only used by {@link + * TableHeader}. + * + * @param e the XML element to which attributes will be added. + */ + final void exportHeadAttributes(Element e) { + super.exportAttributes(e); + } + + /** + * Throw an UnsupportedOperationException. This method can + * only be called if the table column is not properly contained in a + * table. + * + * @param s represents the current request + * @param e the parent element + */ + public void generateXML(PageState s, Element e) { + throw new UnsupportedOperationException("TableColumn used outside of a Table"); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumnModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumnModel.java new file mode 100644 index 000000000..bda366146 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableColumnModel.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.table; + +import java.util.Iterator; + +import com.arsdigita.bebop.SingleSelectionModel; +import com.arsdigita.util.Lockable; + +/** + * Describe interface TableColumnModel here. + * + * @author David Lutterkort + * @version $Id$ + */ +public interface TableColumnModel extends Lockable { + + + void add(TableColumn column); + + TableColumn get(int columnIndex); + + /** + * Insert a column at the given index. The columns from + * columnIndex on are shifted one up. + * + * @param columnIndex the index for the new column. + * @param column the table column to add to the model. + * @pre 0 <= columnIndex && columnIndex <= size() + */ + void add(int columnIndex, TableColumn column); + + void set(int columnIndex, TableColumn v); + + int size(); + + int getIndex(Object columnIdentifier); + + Iterator columns(); + + void remove(TableColumn column); + + SingleSelectionModel getSelectionModel(); + + void setSelectionModel(SingleSelectionModel model); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/TableHeader.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableHeader.java new file mode 100644 index 000000000..8633f6ef4 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableHeader.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.table; + +import static com.arsdigita.bebop.Component.*; + +import java.util.Iterator; + +import javax.servlet.ServletException; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.SimpleComponent; +import com.arsdigita.bebop.Table; +import com.arsdigita.bebop.event.EventListenerList; +import com.arsdigita.bebop.event.TableActionEvent; +import com.arsdigita.bebop.event.TableActionListener; + +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +/** + * This class is used by {@link Table} in order to maintain its headers. + * + * TableHeader is responsible for setting the control event + * in order to notify the {@link Table} when one of the column headers + * is clicked. + * + * @author David Lutterkort + * @version $Id$ + */ +public class TableHeader extends SimpleComponent { + + + /** + * The control event when the user clicks on a column header. + */ + public static final String HEAD_EVENT = "head"; + + private TableCellRenderer m_defaultRenderer; + + private TableColumnModel m_columnModel; + + private Table m_table; + + private EventListenerList m_listeners; + + /** + * Create a new TableHeader + */ + public TableHeader() { + this(new DefaultTableColumnModel()); + } + + /** + * Create a new TableHeader + * + * @param model the {@link TableColumnModel} that the header + * will use in order to generate and maintain the + * column headers. + */ + public TableHeader(TableColumnModel model) { + m_columnModel = model; + m_defaultRenderer = new DefaultTableCellRenderer(); + m_listeners = new EventListenerList(); + } + + /** + * Add an {@link TableActionListener} to the header. + * The listener will be fired whenever this header is + * selected by the user. + * + * @param l the {@link TableActionListener} to add + */ + public void addTableActionListener(TableActionListener l) { + Assert.isUnlocked(this); + m_listeners.add(TableActionListener.class, l); + } + + /** + * Remove a {@link TableActionListener} from the header + * + *@param l the {@link TableActionListener} to remove + */ + public void removeTableActionListener(TableActionListener l) { + Assert.isUnlocked(this); + m_listeners.remove(TableActionListener.class, l); + } + + + /** + * Notify all listeners that the header was selected + * + * @param state the page state + * @param rowKey the key of the selected row, as returned by + * Table.getRowSelectionModel().getSelectedKey(state). + * this key may be null. + * @param column The index of the selected column + * + */ + protected void fireHeadSelected(PageState state, + Object rowKey, Integer column) { + Iterator + i=m_listeners.getListenerIterator(TableActionListener.class); + TableActionEvent e = null; + + while (i.hasNext()) { + if ( e == null ) { + e = new TableActionEvent(this, state, rowKey, column); + } + ((TableActionListener) i.next()).headSelected(e); + } + } + + + /** + * Respond to the current event by selecting the current + * column + * + * @param s the page state + */ + public void respond(PageState s) throws ServletException { + String event = s.getControlEventName(); + if ( HEAD_EVENT.equals(event) ) { + String value = s.getControlEventValue(); + // FIXME: ParameterData allows its value to be set to anything, even + // if it isn't compatible with the ParameterModel + // We need to change ParameterModel/Data to fail earlier on bad data + Integer col = new Integer(value); + getColumnModel().getSelectionModel().setSelectedKey(s, col); + fireHeadSelected(s, null, col); + } else { + throw new ServletException("Unknown event '" + event + "'"); + } + } + + /** + * @return the parent {@link Table} + */ + public final Table getTable() { + return m_table; + } + + /** + * Set the parent {@link Table} + * + * @param v the parent table + */ + public void setTable(Table v) { + Assert.isUnlocked(this); + m_table = v; + } + + /** + * @return the {@link TableColumnModel} which maintains the headers + */ + public final TableColumnModel getColumnModel() { + return m_columnModel; + } + + /** + * Set the {@link TableColumnModel} which will maintain the headers + * + * @param v the new {@link TableColumnModel} + */ + public void setColumnModel(TableColumnModel v) { + Assert.isUnlocked(this); + m_columnModel = v; + } + + /** + * @return the default {@link TableCellRenderer} for this header + */ + public final TableCellRenderer getDefaultRenderer() { + return m_defaultRenderer; + } + + /** + * Set the default {@link TableCellRenderer} for this header. + * Header cells will be rendered with this renderer unless + * the column model specifies an alternative renderer. + * + * @param v the new default renderer + */ + public void setDefaultRenderer(TableCellRenderer v) { + Assert.isUnlocked(this); + m_defaultRenderer = v; + } + + /** + * Generate the XML for this header. The XML will be of the form + *


+     * <bebop:thead>
+     *   <bebop:cell>...</bebop:cell>
+     *   ...
+     * </bebop:thead>
+     * 
+ * + * @param s the page state + * @param p the parent element + */ + public void generateXML(PageState s, Element p) { + if ( isVisible(s) ) { + Element thead = p.newChildElement("bebop:thead", BEBOP_XML_NS); + exportAttributes(thead); + + for (int i=0; i < m_columnModel.size(); i++) { + TableColumn t = m_columnModel.get(i); + + if ( t.isVisible(s) ) { + TableCellRenderer r = t.getHeaderRenderer(); + + if ( r == null ) { + r = getDefaultRenderer(); + } + + boolean isSel = isSelected(s, t.getHeaderKey(), i); + + Component c = r.getComponent(getTable(), s, t.getHeaderValue(), isSel, + t.getHeaderKey(), -1, i); + + if (c != null) { + // supports having a table header disappear + // completely, mainly useful for the odd special case + // where a second-row element is being displayed. + + Element cell = thead.newChildElement("bebop:cell", BEBOP_XML_NS); + t.exportHeadAttributes(cell); + + // Mark the cell as selected if it is selected + if(isSel) { + cell.addAttribute("selected", "1"); + } + + // I added this check so that a table which is not + // added to the Page can still be used to render + // table XML. + + boolean tableIsRegisteredWithPage = + s.getPage().stateContains(getControler()); + + if (tableIsRegisteredWithPage) { + s.setControlEvent(getControler(), HEAD_EVENT, + String.valueOf(i)); + } + + c.generateXML(s, cell); + + if (tableIsRegisteredWithPage) { + s.clearControlEvent(); + } + } + } + } + } + } + + protected Component getControler() { + return this; + } + + /** + * Determine whether the given column is selected. This information + * will be passed to the {@link TableCellRenderer} for this header. + * + * @param s the page state + * @param key the header key for the column as returned by + * TableColumn.getHeaderKey() + * @param column the index of the column to test + */ + protected boolean isSelected(PageState s, Object key, int column) { + if (getTable().getColumnSelectionModel() == null) { + return false; + } + Object sel = getTable() + .getColumnSelectionModel().getSelectedKey(s); + if(sel == null) { + return false; + } + return (column == ((Integer)sel).intValue()); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/TableModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableModel.java new file mode 100644 index 000000000..a1edad592 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableModel.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.table; + +/** + * The TableModel is the abstraction a {@link + * com.arsdigita.bebop.Table Table} uses to access the data it + * displays. The table will ask its {@link TableModelBuilder} to + * instantiate a new table model once for each request it processes. + * + *

The table will request each element in the model at most once per + * request, moving through the rows with successive calls to {@link + * #nextRow}. For each row, the table retrieves the values and keys in each + * column with calls to {@link #getElementAt} and {@link #getKeyAt}. + * + *

The table data is accessed by the table by moving through the rows + * of the table model with calls to {@link #nextRow}. The data for each + * column in a row is represented by two objects: the data element which + * usually contains display information for that column and can be as + * simple as a string, and the key, which is used to identify the + * column. The key is usually a suitable representation of the primary key + * of the underlying object in the database. The key needs to be unique + * amongst all rows in the table model, but doesn't need to + * uniquely identify the row and column for that data item - + * all calls to {@link #getKeyAt} can return the same value for one row in + * the table model. + * + * @see com.arsdigita.bebop.Table Table + * @see TableModelBuilder + * + * @author David Lutterkort + * @version $Id$ */ +public interface TableModel { + + + /** + * Return the number of columns this table model has. + * + * @return the number of columns in the table model + * @post return >= 0 + */ + int getColumnCount(); + + /** + * Move to the next row and return true if the model is now positioned on + * a valid row. Initially, the table model is positioned before the first + * row. The table will call this method before it retrieves the data for + * the row with calls to {@link #getElementAt getElementAt} and {@link + * #getKeyAt getKeyAt}. + * + *

If this method returns true, subsequent calls to + * {@link #getElementAt getElementAt} and {@link #getKeyAt getKeyAt} have + * to succeed and return non-null objects. If this method returns + * false, the table assumes that it has traversed all the + * data contained in this model. + * + * @return true if the model is positioned on a valid row + */ + boolean nextRow(); + + /** + * Return the data element for the given column and the current row. The + * returned object will be passed to the table cell renderer as the + * value argument without modifications. + * + * @param columnIndex the number of the column for which to get data + * @return the object to pass to the table cell renderer for display + * @pre columnIndex >= 0 && columnIndex < getColumnCount() + * @post return != null + * @see TableCellRenderer + */ + Object getElementAt(int columnIndex); + + /** + * Return the key for the given column and the current row. The key has + * to be unique for each row in the table model, but does not + * need to be unique for each row and column, though it may. + * The key is passed to the table cell renderer as the key + * argument. + * + * @param columnIndex the number of the column for which to get data + * @return the key for the given column and the current row. + * @pre columnIndex >= 0 && columnIndex < getColumnCount() + * @post return != null + * @see TableCellRenderer + */ + Object getKeyAt(int columnIndex); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/table/TableModelBuilder.java b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableModelBuilder.java new file mode 100644 index 000000000..3d0851007 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/table/TableModelBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.table; + +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Table; +import com.arsdigita.util.Lockable; + +/** + * Builds the request-specific table models. A table retrieves the data it + * displays by asking the table model builder for a table model. This is + * done for each request; the table does not cahce table models across + * requests. If such caching is desired, it has to be performed by the + * table model builder. + * + *

Typically, the table model builder will run a database query based + * on the information contained in the page state and return the result of + * the database query by wrapping it in a table model. The table will then + * traverse the table model during rendering. + * + *

The table model builder is automatically locked by the table to + * which it was added either through one of the {@link + * com.arsdigita.bebop.Table Table} constructors or with a call to {@link + * com.arsdigita.bebop.Table#setModelBuilder}. + * + * @see com.arsdigita.bebop.Table Table + * @see TableModel + * + * @author David Lutterkort + * @version $Id$ + */ +public interface TableModelBuilder extends Lockable { + + + /** + * Return a table model for the request represented by + * s. The table model contains all the data that is to be + * displayed in a table. The returned table model is used only during + * the duration of that request. + * + * @param t the table which will use this table model + * @param s represents the current request + * @return the data to be displayed in the table + * @pre t != null + * @pre s != null + * @post return != null + */ + TableModel makeModel(Table t, PageState s); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/tree/DefaultTreeCellRenderer.java b/ccm-core/src/main/java/com/arsdigita/bebop/tree/DefaultTreeCellRenderer.java new file mode 100644 index 000000000..1d6b57371 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/tree/DefaultTreeCellRenderer.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.tree; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.ControlLink; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Tree; + +/** + * The interface + * describes how a tree node (component) can be rendered. + * + * @author David Lutterkort + * @author Tri Tran + * + * @see TreeModel + * @see TreeNode + * @see Tree + * @version $Id$ */ +public class DefaultTreeCellRenderer implements TreeCellRenderer { + + /** + * Returns node component to be displayed. The component's + * generateXML or print is called + * to render the node. + * + * @param tree the Tree in which this node is being displayed + * @param state represents the state of the current request + * @param value the object returned by the TreeModel for that node, + * such as pretty name + * @param isSelected true if the node is selected + * @param isExpanded true if the node is expanded (not collapsed) + * @param isLeaf true if the node is a leaf node (no children) + * @param key the object uniquely identify that node (primary key) + * @return the component used to generate the output for the node item + */ + public Component getComponent (Tree tree, PageState state, Object value, + boolean isSelected, boolean isExpanded, + boolean isLeaf, Object key) { + Label l = new Label(value.toString()); + // Bold if selected + if (isSelected) { + l.setFontWeight(Label.BOLD); + return l; + } + // Currently, we are not doing anything special here for + // collapsed/expanded node, or leaf node... Also not doing anything + // fancy with node's key. We are leaving this to Tree.java for now + // to set the appropriate attributes... + return new ControlLink(l); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeCellRenderer.java b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeCellRenderer.java new file mode 100644 index 000000000..a94916130 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeCellRenderer.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.tree; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Tree; + +/** + * The interface + * describes how a tree node (component) can be rendered. + * + * @author David Lutterkort + * @author Tri Tran + * + * @see TreeModel + * @see TreeNode + * @see Tree + * @version $Id$ */ +public interface TreeCellRenderer { + + /** + * Returns node component to be displayed. The component's + * generateXML or print is called + * to render the node. + * + * @param tree the Tree in which this node is being displayed + * @param state represents the state of the current request + * @param value the object returned by the TreeModel for that node + * @param isSelected true if the node is selected + * @param isExpanded true if the node is expanded (not collapsed) + * @param isLeaf true if the node is a leaf node (no children) + * @param key the object uniquely identify that node (primary key) + * @return the component used to generate the output for the node item + */ + Component getComponent (Tree tree, PageState state, Object value, + boolean isSelected, boolean isExpanded, + boolean isLeaf, Object key); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModel.java new file mode 100644 index 000000000..6819fd144 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModel.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.tree; + +import com.arsdigita.bebop.PageState; +import java.util.Iterator; + +/** + * The interface + * describes a model for a tree structure. + * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Tri Tran + * + * @version $Id$ */ +public interface TreeModel { + + + /** + * Obtain the root node of the tree, passing + * in PageState for permissioning purposes + */ + TreeNode getRoot(PageState data); + + /** + * Check whether the node has children + */ + boolean hasChildren(TreeNode n, PageState data); + + /** + * Check whether a given node has children, passing + * in PageState for permissioning purposes + */ + Iterator getChildren(TreeNode n, PageState data); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModelBuilder.java b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModelBuilder.java new file mode 100644 index 000000000..98c481424 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeModelBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.tree; + +import com.arsdigita.bebop.Tree; +import com.arsdigita.bebop.PageState; +import com.arsdigita.util.Lockable; + +/** + * The interface builds a + * {@link TreeModel} for a {@link Tree}. + * + * @author Stanislav Freidin + * + * @version $Id$ */ +public interface TreeModelBuilder extends Lockable { + + + /** + * Builds a {@link TreeModel} to be used in the current request + * + * @param t The {@link Tree} that will use the model + * @param s The page state + */ + TreeModel makeModel(Tree t, PageState s); +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeNode.java b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeNode.java new file mode 100644 index 000000000..4e29232f2 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/tree/TreeNode.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop.tree; + +/** + * The interface + * describes a node for a tree. + * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Tri Tran + * + * @version $Id$ */ +public interface TreeNode { + + + /** + * Obtain a unique ID representing the node + */ + Object getKey(); + + /** + * Get the element of the tree node. The concrete type + * of the object returned is specific to each implementation + * of the TreeModel and should be documented + * there. + * + * @return the element for the tree node + */ + Object getElement(); +} diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/AccessDeniedException.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/AccessDeniedException.java new file mode 100644 index 000000000..5a12afb87 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/AccessDeniedException.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.dispatcher; + +import javax.servlet.http.HttpServletRequest; + + +/** + * AccessDeniedException is the runtime exception that is thrown + * whenever the current user does not have access to the requested resources. + * + * @author Michael Pih + * @version $Id$ + */ +public class AccessDeniedException extends RuntimeException { + + + public final static String ACCESS_DENIED = + "com.arsdigita.cms.dispatcher.AccessDeniedException"; + + + // The default error detail message. + private final static String ERROR_MSG = "Access Denied"; + + // The URL where the AccessDeniedException is thrown. + private String m_url; + + + /** + * Constructs an AccessDeniedException with the default detail message. + */ + public AccessDeniedException() { + this(ERROR_MSG); + } + + /** + * Constructs an AccessDeniedException with the specified detail message. + * + * @param msg The error detail message + */ + public AccessDeniedException(String msg) { + super(msg); + + // Try and fetch the current request URL. + HttpServletRequest request = DispatcherHelper.getRequest(); + if ( request != null ) { + m_url = DispatcherHelper.getRequest().getRequestURI(); + request.setAttribute(ACCESS_DENIED, m_url); + } else { + m_url = null; + } + } + + /** + * Fetches the URL where the AccessDeniedException originated. + * + * @return The original URL + */ + public String getOriginalURL() { + return m_url; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/toolbox/ui/ComponentMap.java b/ccm-core/src/main/java/com/arsdigita/toolbox/ui/ComponentMap.java new file mode 100644 index 000000000..a253cf964 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/toolbox/ui/ComponentMap.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.toolbox.ui; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Resettable; +import com.arsdigita.bebop.SimpleComponent; +import com.arsdigita.util.Assert; +import com.arsdigita.util.SequentialMap; +import com.arsdigita.xml.Element; +import java.util.Iterator; +import org.apache.log4j.Logger; + +/** + * + * @version $Id$ + */ +public abstract class ComponentMap extends SimpleComponent + implements Resettable { + + private static final Logger s_log = Logger.getLogger(ComponentMap.class); + + private final SequentialMap m_components; + + public ComponentMap() { + m_components = new SequentialMap(); + } + + public final Iterator children() { + return m_components.values().iterator(); + } + + public void reset(final PageState state) { + s_log.debug("Resetting my children"); + + final Iterator iter = children(); + + while (iter.hasNext()) { + final Object component = iter.next(); + + if (component instanceof Resettable) { + ((Resettable) component).reset(state); + } + } + } + + public final void put(final Object key, final Component component) { + Assert.isUnlocked(this); + Assert.exists(key, Object.class); + + m_components.put(key, component); + } + + public final Component get(final Object key) { + return (Component) m_components.get(key); + } + + public final boolean containsKey(final Object key) { + return m_components.containsKey(key); + } + + public final boolean containsValue(final Component component) { + return m_components.containsValue(component); + } + + public abstract void generateXML(final PageState state, + final Element parent); +} diff --git a/ccm-core/src/main/java/com/arsdigita/toolbox/ui/LayoutPanel.java b/ccm-core/src/main/java/com/arsdigita/toolbox/ui/LayoutPanel.java new file mode 100644 index 000000000..6ff00a0cc --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/toolbox/ui/LayoutPanel.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.toolbox.ui; + +import static com.arsdigita.bebop.Component.*; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.PageState; +import com.arsdigita.xml.Element; + +import org.apache.log4j.Logger; + +/** + *

A simple layout panel with top, bottom, left, right, and body + * sections.

+ * + * @author Justin Ross <jross@redhat.com> + * @version $Id$ + */ +public class LayoutPanel extends ComponentMap { + + private static final Logger s_log = Logger.getLogger(LayoutPanel.class); + + public final void setTop(final Component top) { + put("top", top); + } + + public final void setLeft(final Component left) { + put("left", left); + } + + public final void setBody(final Component body) { + put("body", body); + } + + public final void setRight(final Component right) { + put("right", right); + } + + public final void setBottom(final Component bottom) { + put("bottom", bottom); + } + + @Override + public void generateXML(final PageState state, final Element parent) { + if (isVisible(state)) { + final Element layout = parent.newChildElement + ("bebop:layoutPanel", BEBOP_XML_NS); + + section(state, layout, "top"); + section(state, layout, "left"); + section(state, layout, "body"); + section(state, layout, "right"); + section(state, layout, "bottom"); + } + } + + private void section(final PageState state, + final Element parent, + final String key) { + final Component section = get(key); + + if (section != null) { + final Element elem = parent.newChildElement + ("bebop:" + key, BEBOP_XML_NS); + + section.generateXML(state, elem); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/AdminConstants.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/AdminConstants.java new file mode 100644 index 000000000..638e9bb6a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/AdminConstants.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.parameters.BigDecimalParameter; +import com.arsdigita.globalization.GlobalizedMessage; + +/** + * Centralize place for all constants used in the admin UI. + * + * @author David Dao + * @version $Revision$ $Date$ + */ +interface AdminConstants { + + /** + * The XML namespace used by admin components. + */ + String ADMIN_XML_NS = "http://www.arsdigita.com/admin-ui/1.0"; + + /** + * Globalization resource for admin ui. + */ + String BUNDLE_NAME = "com.arsdigita.ui.admin.AdminResources"; + + /** + * Navigational dimension bar labels. + */ + // Label MY_WORKSPACE_LABEL = new Label + // (new GlobalizedMessage("ui.admin.nav.workspace", + // BUNDLE_NAME)); + // Label LOG_OUT_LABEL = new Label + // (new GlobalizedMessage("ui.admin.nav.logout", + // BUNDLE_NAME)); + /** + * Administration page title + */ + Label PAGE_TITLE_LABEL = new Label(new GlobalizedMessage("ui.admin.dispatcher.title", + BUNDLE_NAME)); + + /** + * Administration main tab names. + */ + Label USER_TAB_TITLE = new Label(new GlobalizedMessage("ui.admin.tab.user", + BUNDLE_NAME)); + + Label GROUP_TAB_TITLE = new Label(new GlobalizedMessage("ui.admin.tab.group", + BUNDLE_NAME)); + + Label APPLICATIONS_TAB_TITLE = new Label(new GlobalizedMessage("ui.admin.tab.applications", + BUNDLE_NAME)); + + Label SYSINFO_TAB_TITLE = new Label(new GlobalizedMessage("ui.admin.tab.sysinfo.title", BUNDLE_NAME)); + + GlobalizedMessage USER_NAVBAR_TITLE = new GlobalizedMessage("ui.admin.tab.user.navbartitle", + BUNDLE_NAME); + + /** + * Tabbed pane indices + */ + int USER_TAB_INDEX = 2; + int GROUP_TAB_INDEX = 3; + + /** + * User tab name + */ + Label USER_TAB_SUMMARY = new Label(new GlobalizedMessage("ui.admin.tab.user.summary", + BUNDLE_NAME)); + Label USER_TAB_BROWSE = new Label(new GlobalizedMessage("ui.admin.tab.user.browse", + BUNDLE_NAME)); + Label USER_TAB_SEARCH = new Label(new GlobalizedMessage("ui.admin.tab.user.search", + BUNDLE_NAME)); + Label USER_TAB_CREATE_USER = new Label(new GlobalizedMessage("ui.admin.tab.user.createuser", + BUNDLE_NAME)); + + int USER_TAB_SUMMARY_INDEX = 0; + int USER_TAB_BROWSE_INDEX = 1; + int USER_TAB_SEARCH_INDEX = 2; + int USER_TAB_CREATE_USER_INDEX = 3; + + /** + * Global state parameters. + */ + BigDecimalParameter GROUP_ID_PARAM = new BigDecimalParameter("group_id"); + + BigDecimalParameter APPLICATIONS_ID_PARAM = new BigDecimalParameter("application_id"); + + BigDecimalParameter USER_ID_PARAM = new BigDecimalParameter("user_id"); + + /** + * User summary panel. + */ + Label SUMMARY_PANEL_HEADER = new Label( + new GlobalizedMessage("ui.admin.user.summarypanel.header", BUNDLE_NAME)); + + Label CREATE_USER_LABEL = new Label(new GlobalizedMessage( + "ui.admin.user.summarypanel.createUser", BUNDLE_NAME)); + + Label TOTAL_USERS_LABEL = new Label(new GlobalizedMessage( + "ui.admin.user.summarypanel.totalusers", BUNDLE_NAME)); + + /** + * User browse panel. + */ + Label BROWSE_USER_PANEL_HEADER = new Label(new GlobalizedMessage( + "ui.admin.user.browsepanel.header", + BUNDLE_NAME)); + + Label USER_INFO_LABEL = new Label(new GlobalizedMessage("ui.admin.user.userinfo.header", + BUNDLE_NAME)); + + Label USER_EDIT_PANEL_HEADER = new Label(new GlobalizedMessage("ui.admin.user.useredit.header", + BUNDLE_NAME)); + + Label USER_GROUP_PANEL_HEADER = new Label(new GlobalizedMessage( + "ui.admin.user.groupmembership.header", + BUNDLE_NAME)); + + Label USER_DELETE_FAILED_PANEL_HEADER = new Label(new GlobalizedMessage( + "ui.admin.user.action.delete.failed.header", + BUNDLE_NAME)); + + Label USER_PASSWORD_PANEL_HEADER = new Label(new GlobalizedMessage( + "ui.admin.user.password.header", + BUNDLE_NAME)); + + Label USER_ACTION_PANEL_HEADER = new Label(new GlobalizedMessage("ui.admin.user.action.header", + BUNDLE_NAME)); + + Label USER_ACTION_CONTINUE = new Label(new GlobalizedMessage("ui.admin.user.action.continue", + BUNDLE_NAME)); + + Label USER_DELETE_LABEL = new Label(new GlobalizedMessage("ui.admin.user.delete.label", + BUNDLE_NAME)); + + Label USER_BAN_LABEL = new Label(new GlobalizedMessage("ui.admin.user.ban.label", + BUNDLE_NAME)); + + Label USER_UNBAN_LABEL = new Label(new GlobalizedMessage("ui.admin.user.unban.label", + BUNDLE_NAME)); + + GlobalizedMessage USER_DELETE_CONFIRMATION = new GlobalizedMessage( + "ui.admin.user.delete.confirm", BUNDLE_NAME); + + GlobalizedMessage USER_BAN_CONFIRMATION = new GlobalizedMessage("ui.admin.user.ban.confirm", + BUNDLE_NAME); + + GlobalizedMessage USER_UNBAN_CONFIRMATION = new GlobalizedMessage("ui.admin.user.unban.confirm", + BUNDLE_NAME); + + GlobalizedMessage USER_DELETE_FAILED_MSG = new GlobalizedMessage( + "ui.admin.user.delete.failed.label", BUNDLE_NAME); + + Label USER_TAB_EXTREME_ACTION_LABEL = new Label(new GlobalizedMessage( + "ui.admin.user.browsepanel.extremeaction", + BUNDLE_NAME)); + + Label UPDATE_USER_PASSWORD_LABEL = new Label(new GlobalizedMessage( + "ui.admin.user.browsepanel.updatePassword", + BUNDLE_NAME)); + + Label BECOME_USER_LABEL = new Label( + new GlobalizedMessage("ui.admin.user.browsepanel.becomeUser", + BUNDLE_NAME)); + + /** + * Create new user panel. + */ + Label CREATE_USER_PANEL_HEADER = new Label(new GlobalizedMessage( + "ui.admin.user.createpanel.header", + BUNDLE_NAME)); + + /** + * User search panel. + */ + Label SEARCH_PANEL_HEADER = new Label(new GlobalizedMessage("ui.admin.user.search.header", + BUNDLE_NAME)); + + Label PASSWORD_FORM_LABEL_PASSWORD = new Label(new GlobalizedMessage( + "ui.admin.user.userpasswordform.passwordlabel", + BUNDLE_NAME)); + + Label PASSWORD_FORM_LABEL_CONFIRMATION_PASSWORD = new Label(new GlobalizedMessage( + "ui.admin.user.userpasswordform.confirmpasswordlabel", + BUNDLE_NAME)); + + Label PASSWORD_FORM_LABEL_QUESTION = new Label(new GlobalizedMessage( + "ui.admin.user.userpasswordform.question", + BUNDLE_NAME), false); + + Label PASSWORD_FORM_LABEL_ANSWER = new Label(new GlobalizedMessage( + "ui.admin.user.userpasswordform.answer", + BUNDLE_NAME), false); + + GlobalizedMessage PASSWORD_FORM_SUBMIT = new GlobalizedMessage( + "ui.admin.user.userpasswordform.submit", + BUNDLE_NAME); + + /** + * Constants for user add/edit form. + */ + String USER_FORM_ADD = "user-add-form"; + String USER_FORM_EDIT = "user-edit-form"; + String USER_FORM_INPUT_FIRST_NAME = "firstname"; + String USER_FORM_INPUT_LAST_NAME = "lastname"; + String USER_FORM_INPUT_PASSWORD = "password"; + String USER_FORM_INPUT_PASSWORD_CONFIRMATION = "password_confirmation"; + String USER_FORM_INPUT_QUESTION = "question"; + String USER_FORM_INPUT_ANSWER = "answer"; + String USER_FORM_INPUT_PRIMARY_EMAIL = "email"; + String USER_FORM_INPUT_ADDITIONAL_EMAIL = "additional_email"; + String USER_FORM_INPUT_SCREEN_NAME = "screenname"; + String USER_FORM_INPUT_SSO = "sso_login"; + String USER_FORM_INPUT_URL = "url"; + String USER_FORM_INPUT_URL_DEFAULT = "http://"; + + Label USER_FORM_LABEL_FIRST_NAME = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.firstname", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_LAST_NAME = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.lastname", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_PASSWORD = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.password", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_PASSWORD_CONFIRMATION = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.confirmation", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_QUESTION = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.question", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_ANSWER = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.answer", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_PRIMARY_EMAIL = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.primaryemail", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_ADDITIONAL_EMAIL = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.additionalemail", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_ADDITIONAL_EMAIL_LIST = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.additionalemaillist", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_SCREEN_NAME = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.screenname", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_SSO = new Label(new GlobalizedMessage( + "ui.admin.user.addeditform.ssologinname", + BUNDLE_NAME)); + + Label USER_FORM_LABEL_URL = new Label(new GlobalizedMessage("ui.admin.user.addeditform.url", + BUNDLE_NAME)); + + Label USER_FORM_DELETE_ADDITIONAL_EMAIL = new Label( + new GlobalizedMessage("ui.admin.user.addeditform.deleteemail", + BUNDLE_NAME)); + + GlobalizedMessage USER_FORM_SUBMIT = new GlobalizedMessage("ui.admin.user.addeditform.submit", + BUNDLE_NAME); + + GlobalizedMessage USER_FORM_ERROR_SCREEN_NAME_NOT_UNIQUE = new GlobalizedMessage( + "ui.admin.user.addeditform.error.screenname.notunique", + BUNDLE_NAME); + + GlobalizedMessage USER_FORM_ERROR_PRIMARY_EMAIL_NOT_UNIQUE = new GlobalizedMessage( + "ui.admin.user.addeditform.error.primaryemail.notunique", + BUNDLE_NAME); + + GlobalizedMessage USER_FORM_ERROR_PASSWORD_NOT_MATCH = new GlobalizedMessage( + "ui.admin.user.addeditform.error.password.notmatch", + BUNDLE_NAME); + + GlobalizedMessage USER_FORM_ERROR_ANSWER_NULL = new GlobalizedMessage( + "ui.admin.user.addeditform.error.answer.null", + BUNDLE_NAME); + + GlobalizedMessage USER_FORM_ERROR_ANSWER_WHITESPACE = new GlobalizedMessage( + "ui.admin.user.addeditform.error.answer.whitespace", + BUNDLE_NAME); + + /** + * Constants for group add/edit form. + */ + String GROUP_FORM_ADD = "group-add-form"; + String GROUP_FORM_EDIT = "group-edit-form"; + String GROUP_FORM_INPUT_NAME = "name"; + String GROUP_FORM_INPUT_PRIMARY_EMAIL = "email"; + + Label GROUP_FORM_LABEL_NAME = new Label(new GlobalizedMessage( + "ui.admin.groups.addeditform.namelabel", + BUNDLE_NAME)); + + Label GROUP_FORM_LABEL_PRIMARY_EMAIL = new Label(new GlobalizedMessage( + "ui.admin.groups.addeditform.primaryemaillabel", + BUNDLE_NAME)); + + GlobalizedMessage GROUP_FORM_SUBMIT + = new GlobalizedMessage("ui.admin.groups.addeditform.submit", BUNDLE_NAME); + + /** + * Constants for group administration tab. + */ + Label GROUP_ACTION_CONTINUE = new Label(new GlobalizedMessage("ui.admin.groups.actioncontinue", + BUNDLE_NAME)); + + GlobalizedMessage GROUP_DELETE_FAILED_MSG = new GlobalizedMessage( + "ui.admin.groups.groupdeletefailed", + BUNDLE_NAME); + + Label GROUP_INFORMATION_HEADER = new Label(new GlobalizedMessage( + "ui.admin.groups.groupinformation", + BUNDLE_NAME)); + + Label SUBGROUP_HEADER = new Label(new GlobalizedMessage("ui.admin.groups.subgroups", + BUNDLE_NAME)); + + Label GROUP_EDIT_HEADER = new Label(new GlobalizedMessage("ui.admin.groups.groupedit", + BUNDLE_NAME)); + Label ADD_SUBGROUP_LABEL = new Label(new GlobalizedMessage("ui.admin.groups.add", + BUNDLE_NAME)); + + Label SUBMEMBER_HEADER = new Label(new GlobalizedMessage("ui.admin.groups.submembers", + BUNDLE_NAME)); + + Label DELETE_GROUP_LABEL = new Label(new GlobalizedMessage("ui.admin.groups.delete", + BUNDLE_NAME)); + + Label GROUP_EXTREME_ACTIONS_HEADER = new Label(new GlobalizedMessage( + "ui.admin.groups.extremeaction", + BUNDLE_NAME)); + + Label GROUP_DELETE_FAILED_HEADER = new Label(new GlobalizedMessage( + "ui.admin.groups.deletefailed", + BUNDLE_NAME)); + + Label ADD_GROUP_LABEL = new Label(new GlobalizedMessage("ui.admin.groups.addgrouplabel", + BUNDLE_NAME)); + Label EDIT_GROUP_LABEL = new Label(new GlobalizedMessage("ui.admin.groups.edit", + BUNDLE_NAME)); + + Label SUBGROUP_COUNT_LABEL = new Label(new GlobalizedMessage( + "ui.admin.groups.subgroupcountlabel", + BUNDLE_NAME)); + String GROUP_DELETE_CONFIRMATION = "Are you sure you want to delete this group?"; + + Label ADD_SUBMEMBER_LABEL = new Label(new GlobalizedMessage("ui.admin.groups.addsubmemberlabel", + BUNDLE_NAME)); + + Label REMOVE_SUBMEMBER_LABEL = new Label(new GlobalizedMessage( + "ui.admin.groups.removesubmemberlabel", + BUNDLE_NAME)); + Label ADD_EXISTING_GROUP_TO_SUBGROUPS_LABEL = new Label(new GlobalizedMessage( + "ui.admin.groups.addExisting", + BUNDLE_NAME)); + + Label REMOVE_SUBGROUP_LABEL = new Label(new GlobalizedMessage("ui.admin.groups.removeExisting", + BUNDLE_NAME)); + + Label GROUP_SEARCH_LABEL = new Label( + new GlobalizedMessage("ui.admin.groups.search", BUNDLE_NAME)); + + GlobalizedMessage SEARCH_BUTTON = new GlobalizedMessage("ui.admin.groups.button.search", + BUNDLE_NAME); + + Label GROUP_NO_RESULTS = new Label(new GlobalizedMessage("ui.admin.groups.searchForm.noResults", + BUNDLE_NAME)); + + Label FOUND_GROUPS_TITLE = new Label(new GlobalizedMessage("ui.admin.groups.found.title", + BUNDLE_NAME)); + + Label PICK_GROUPS = new Label(new GlobalizedMessage("ui.admin.groups.select.explanation", + BUNDLE_NAME)); + + GlobalizedMessage SAVE_BUTTON = new GlobalizedMessage("ui.admin.save", BUNDLE_NAME); + + String SEARCH_QUERY = "query"; + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/AdminServlet.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/AdminServlet.java new file mode 100644 index 000000000..60c8162ea --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/AdminServlet.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2012 Peter Boy All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.Page; +import com.arsdigita.bebop.PageFactory; +import com.arsdigita.bebop.TabbedPane; +import com.arsdigita.dispatcher.AccessDeniedException; +import com.arsdigita.dispatcher.DispatcherHelper; +import com.arsdigita.templating.Templating; +import com.arsdigita.util.Assert; +import com.arsdigita.util.UncheckedWrapperException; +import com.arsdigita.web.BaseApplicationServlet; +import com.arsdigita.web.LoginSignal; +import com.arsdigita.xml.Document; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.CcmSessionContext; +import org.libreccm.core.PermissionManager; +import org.libreccm.core.Privilege; +import org.libreccm.core.PrivilegeRepository; +import org.libreccm.core.Subject; +import org.libreccm.web.CcmApplication; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Web Developer Support Application Servlet class, central entry point to + * create and process the applications UI. + * + * We should have subclassed BebopApplicationServlet but couldn't overwrite + * doService() method to add permission checking. So we use our own page + * mapping. The general logic is the same as for BebopApplicationServlet. + * + * { + * + * @see com.arsdigita.bebop.page.BebopApplicationServlet} + * + * @author Jens Pelzetter + * @author pb + */ +public class AdminServlet extends BaseApplicationServlet implements + AdminConstants { + + private static final long serialVersionUID = -3912367600768871630L; + /** + * Logger instance for debugging + */ + //private static final Logger LOGGER = Logger.getLogger(AdminServlet.class.getName()); + /** + * URL (pathinfo) -> Page object mapping. Based on it (and the http request + * url) the doService method to selects a page to display + */ + private final Map pages = new HashMap(); + + /** + * User extension point, overwrite this method to setup a URL - page mapping + * + * @throws ServletException + */ + @Override + public void doInit() throws ServletException { + addPage("/", buildAdminIndexPage()); // index page at address ~/ds + // addPage("/index.jsp", buildIndexPage()); // index page at address ~/ds + + } + + /** + * Central service method, checks for required permission, determines the + * requested page and passes the page object to PresentationManager. + * + * @param sreq + * @param sresp + * @param app + * + * @throws ServletException + * @throws IOException + */ + @Override + public final void doService(final HttpServletRequest sreq, + final HttpServletResponse sresp, + final CcmApplication app) throws + ServletException, IOException { + // /////// Some preparational steps /////////////// + /* Determine access privilege: only logged in users may access DS */ + final CdiUtil cdiUtil = new CdiUtil(); + final CcmSessionContext sessionContext; + try { + sessionContext = cdiUtil.findBean( + CcmSessionContext.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup session context", ex); + } + final Subject subject = sessionContext.getCurrentSubject(); + if (subject == null) { + throw new LoginSignal(sreq); + } + + final PrivilegeRepository privilegeRepository; + try { + privilegeRepository = cdiUtil.findBean(PrivilegeRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup PrivilegeRepository", ex); + } + final Privilege adminPrivilege = privilegeRepository.retrievePrivilege( + "admin"); + + final PermissionManager permissionManager; + try { + permissionManager = cdiUtil.findBean(PermissionManager.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to look up PermissionManager", ex); + } + + if (!permissionManager.isPermitted(adminPrivilege, null, subject)) { + throw new AccessDeniedException("User is not an administrator"); + } + + /* Want admin to always show the latest stuff... */ + DispatcherHelper.cacheDisable(sresp); + + // /////// Everything OK here - DO IT /////////////// + String pathInfo = sreq.getPathInfo(); + Assert.exists(pathInfo, "String pathInfo"); + if (pathInfo.length() > 1 && pathInfo.endsWith("/")) { + /* NOTE: ServletAPI specifies, pathInfo may be empty or will + * start with a '/' character. It currently carries a + * trailing '/' if a "virtual" page, i.e. not a real jsp, but + * result of a servlet mapping. But Application requires url + * NOT to end with a trailing '/' for legacy free applications. */ + pathInfo = pathInfo.substring(0, pathInfo.length() - 1); + } + + final Page page = pages.get(pathInfo); + if (page == null) { + sresp.sendError(404, "No such page for path " + pathInfo); + } else { + final Document doc = page.buildDocument(sreq, sresp); + Templating.getPresentationManager().servePage(doc, sreq, sresp); + } + } + + /** + * Adds one pair of Url - Page to the internal hash map, used as a cache. + * + * @param pathInfo url stub for a page to display + * @param page Page object to display + */ + private void addPage(final String pathInfo, final Page page) { + Assert.exists(pathInfo, String.class); + Assert.exists(page, Page.class); + // Current Implementation requires pathInfo to start with a leading '/' + // SUN Servlet API specifies: "PathInfo *may be empty* or will start + // with a '/' character." + Assert.isTrue(pathInfo.charAt(0) == '/', "path starts not with '/'"); + + pages.put(pathInfo, page); + } + + /** + * Index page for the admin section + */ + private Page buildAdminIndexPage() { + + final Page page = PageFactory.buildPage("admin", PAGE_TITLE_LABEL); + page.addGlobalStateParam(USER_ID_PARAM); + page.addGlobalStateParam(GROUP_ID_PARAM); + page.addGlobalStateParam(APPLICATIONS_ID_PARAM); + + /* + * Create User split panel. + * Note: Will change soon. + */ + //final AdminSplitPanel userSplitPanel = new AdminSplitPanel(USER_NAVBAR_TITLE); +// final UserBrowsePane browsePane = new UserBrowsePane(); +// userSplitPanel.addTab(USER_TAB_SUMMARY, new UserSummaryPane(userSplitPanel, browsePane)); +// userSplitPanel.addTab(USER_TAB_BROWSE, browsePane); +// userSplitPanel.addTab(USER_TAB_SEARCH, new UserSearchPane(userSplitPanel, browsePane)); +// userSplitPanel.addTab(USER_TAB_CREATE_USER, new CreateUserPane(userSplitPanel)); + // Create the Admin's page tab bar + final TabbedPane tabbedPane = new TabbedPane(); + tabbedPane.setIdAttr("page-body"); + + /** + * Create and add info tab + */ + //tabbedPane.addTab(INFO_TAB_TITLE, new AdminInfoTab()); + /* + * Create and add the user and group tabs. + */ + //tabbedPane.addTab(USER_TAB_TITLE, userSplitPanel); + final GroupAdministrationTab groupAdminTab + = new GroupAdministrationTab(); + tabbedPane.addTab(USER_TAB_TITLE, new UserAdministrationTab(tabbedPane, + groupAdminTab)); + tabbedPane.addTab(GROUP_TAB_TITLE, groupAdminTab); + + /* + * Create application administration panel + */ + // ToDo tabbedPane.addTab(APPLICATIONS_TAB_TITLE, + // ToDo new ApplicationsAdministrationTab()); + +// browsePane.setTabbedPane(tabbedPane); +// browsePane.setGroupAdministrationTab(groupAdminTab); + //Add System information tab + // ToDo tabbedPane.addTab(SYSINFO_TAB_TITLE, new SystemInformationTab()); + + page.add(tabbedPane); + page.lock(); + + return page; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/EmailList.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/EmailList.java new file mode 100644 index 000000000..cc430a840 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/EmailList.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.ControlLink; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.List; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.SimpleContainer; +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.bebop.list.ListCellRenderer; +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.util.LockableImpl; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.EmailAddress; +import org.libreccm.core.User; +import org.libreccm.core.UserRepository; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +import java.math.BigDecimal; +import java.util.Iterator; + +/** + * Used to display and manage the list of additional email addresses + * for a user. + */ + +class EmailList extends List + implements ListCellRenderer, + AdminConstants, + ActionListener +{ + + /** + * Constructor + */ + public EmailList() { + setModelBuilder(new EmailListModelBuilder()); + setCellRenderer(this); + addActionListener(this); + } + + /** + * + * @param list + * @param state + * @param value + * @param key + * @param index + * @param isSelected + * @return + */ + @Override + public Component getComponent(List list, + PageState state, + Object value, + String key, + int index, + boolean isSelected) + { + SimpleContainer c = new SimpleContainer(); + + if (value != null) { + ControlLink link = + new ControlLink(USER_FORM_DELETE_ADDITIONAL_EMAIL); + link.setClassAttr("deleteLink"); + + c.add(new Label(value.toString())); + c.add(link); + } + + return c; + } + + /** + * This actionlister is executed when the user clicks the "delete" + * link next to an email address. + */ + @Override + public void actionPerformed (final ActionEvent event) { + final PageState state = event.getPageState(); + + final Long userId = (Long) state.getValue(USER_ID_PARAM); + if (userId != null) { + final CdiUtil cdiUtil = new CdiUtil(); + final UserRepository userRepository; + try { + userRepository = cdiUtil.findBean(UserRepository.class); + } catch(CdiLookupException ex) { + throw new UncheckedWrapperException(ex); + } + + final User user = userRepository.findById(userId); + if (user == null) { + return; + } else { + final String email = (String) getSelectedKey(state); + + for(EmailAddress addr : user.getEmailAddresses()) { + if (addr.getAddress().equals(email)) { + user.removeEmailAddress(addr); + } + } + + userRepository.save(user); + } + } + } +} + +/** + * + * + */ +class EmailListModelBuilder extends LockableImpl + implements ListModelBuilder, + AdminConstants +{ + + /** + * + */ + private class EmailListModel implements ListModel { + private Iterator m_emails; + private EmailAddress m_currentEmail; + + /** + * + * @param emails + */ + public EmailListModel(Iterator emails) { + m_emails = emails; + } + + /** + * + * @return + */ + public boolean next() { + if (m_emails.hasNext()) { + m_currentEmail = (EmailAddress) m_emails.next(); + return true; + } else { + return false; + } + } + + /** + * + * @return + */ + @Override + public String getKey() { + return m_currentEmail.getAddress(); + } + + @Override + public Object getElement() { + return m_currentEmail.getAddress(); + } + } + + /** + * + * @param l + * @param state + * @return + */ + @Override + public ListModel makeModel(List l, PageState state) { + + final Long userId = (Long) state.getValue(USER_ID_PARAM); + if (userId == null) { + return null; + } else { + final CdiUtil cdiUtil = new CdiUtil(); + final UserRepository userRepository; + try { + userRepository = cdiUtil.findBean(UserRepository.class); + } catch(CdiLookupException ex) { + throw new UncheckedWrapperException(ex); + } + final User user = userRepository.findById(userId); + + return new EmailListModel(user.getEmailAddresses().iterator()); + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/ExistingGroupAddPane.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/ExistingGroupAddPane.java new file mode 100644 index 000000000..33b68162d --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/ExistingGroupAddPane.java @@ -0,0 +1,204 @@ +package com.arsdigita.ui.admin; + +import java.math.BigDecimal; + +import org.apache.log4j.Logger; + +import com.arsdigita.bebop.BoxPanel; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.Page; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.RequestLocal; +import com.arsdigita.bebop.SegmentedPanel; +import com.arsdigita.bebop.SimpleContainer; +import com.arsdigita.bebop.Tree; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupRepository; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +/** + * Series of screens required for adding existing groups as subgroups - based on + * existing functionality for adding permissions to a folder in content/admin + * + * @version $Id: ExistingGroupAddPane.java,v 1.4 2004/06/21 11:34:03 cgyg9330 + * Exp $ + */ +public class ExistingGroupAddPane extends SimpleContainer implements + AdminConstants { + + private static final Logger s_log = Logger.getLogger( + ExistingGroupAddPane.class); + + private ParameterModel searchString = new StringParameter(SEARCH_QUERY); + + private GroupSearchForm groupSearchForm; + private SimpleContainer selectGroupsPanel; + private SimpleContainer noResultsPanel; + private Tree groupTree; + private GroupAdministrationTab parentPage; + + /** + * + */ + private RequestLocal parentGroup = new RequestLocal() { + + @Override + protected Object initialValue(final PageState ps) { + String key = (String) groupTree.getSelectedKey(ps); + + Group group = null; + + if (key != null) { + final Long id = new Long(key); + + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + + group = groupRepository.findById(id); + } + + return group; + } + + }; + + /** + * Constructor. + * + * @param groupTree + * @param parentPage + */ + public ExistingGroupAddPane(Tree groupTree, + GroupAdministrationTab parentPage) { + this.groupTree = groupTree; + this.parentPage = parentPage; + } + + /** + * + * @param p + */ + @Override + public void register(Page p) { + super.register(p); + add(getGroupSearchForm()); + add(getSelectGroupsPanel()); + add(getNoSearchResultPanel()); + + // set initial visibility of components + p.setVisibleDefault(getGroupSearchForm(), true); + p.setVisibleDefault(getSelectGroupsPanel(), false); + p.setVisibleDefault(getNoSearchResultPanel(), false); + + p.addGlobalStateParam(searchString); + + } + + /** + * + * @return + */ + public GroupSearchForm getGroupSearchForm() { + + if (groupSearchForm == null) { + groupSearchForm = new GroupSearchForm(this); + } + return groupSearchForm; + } + + /** + * Returns a panel with a set of checkboxes for groups fulfilling search + * criteria + */ + public SimpleContainer getSelectGroupsPanel() { + if (selectGroupsPanel == null) { + SelectGroups selectGroups = new SelectGroups(this, + getGroupSearchForm()); + selectGroupsPanel = selectGroups.getPanel(); + } + return selectGroupsPanel; + } + + /** + * Returns a bebop panel indicating that the user search yielded no results. + */ + public SimpleContainer getNoSearchResultPanel() { + if (noResultsPanel == null) { + Label errorMsg = GROUP_NO_RESULTS; + errorMsg.setClassAttr("errorBullet"); + BoxPanel bp = new BoxPanel(); + bp.add(errorMsg); + bp.add(new GroupSearchForm(this)); + noResultsPanel = new SegmentedPanel().addSegment(new Label(" "), bp); + } + return noResultsPanel; + } + + /** + * Shows panel with no results to user search. + */ + public void showNoResults(PageState s) { + getGroupSearchForm().setVisible(s, false); + getSelectGroupsPanel().setVisible(s, false); + getNoSearchResultPanel().setVisible(s, true); + } + + /** + * Show the select groups to add as subgroups panel + */ + public void showGroups(PageState s) { + getGroupSearchForm().setVisible(s, false); + getSelectGroupsPanel().setVisible(s, true); + getNoSearchResultPanel().setVisible(s, false); + } + + /** + * + * show the search form + */ + public void showSearch(PageState s) { + getGroupSearchForm().setVisible(s, true); + getSelectGroupsPanel().setVisible(s, false); + getNoSearchResultPanel().setVisible(s, false); + } + + /** + * + * @return + */ + public ParameterModel getSearchString() { + return searchString; + } + + /** + * + * @return + */ + public GroupAdministrationTab getParentPage() { + return parentPage; + } + + /** + * + * @param ps + * + * @return + */ + public Group getParentGroup(PageState ps) { + return (Group) parentGroup.get(ps); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GlobalizationUtil.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GlobalizationUtil.java new file mode 100644 index 000000000..0641f5c4a --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GlobalizationUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 Jens Pelzetter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.globalization.GlobalizedMessage; + +/** + * Globalization Util for admin classes + * + * @author Jens Pelzetter + * @version $Id$ + */ +public class GlobalizationUtil { + + private static final String BUNDLE_NAME = "com.arsdigita.ui.admin.AdminResources"; + + public static GlobalizedMessage globalize(final String key) { + return new GlobalizedMessage(key, BUNDLE_NAME); + } + + public static GlobalizedMessage globalize(final String key, final Object[] args) { + return new GlobalizedMessage(key, BUNDLE_NAME, args); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAddForm.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAddForm.java new file mode 100644 index 000000000..3967d0f4f --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAddForm.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Tree; + +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupRepository; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +import java.math.BigDecimal; + +import javax.mail.internet.InternetAddress; + +/** + * Add group form. + * + * @author David Dao + * @version $Id$ + */ +class GroupAddForm extends GroupForm implements FormProcessListener { + + private Tree m_groupTree; + private GroupAdministrationTab m_groupTab; + + public GroupAddForm(final Tree groupTree, + final GroupAdministrationTab tab) { + super(GROUP_FORM_ADD); + addProcessListener(this); + m_groupTree = groupTree; + m_groupTab = tab; + } + + /** + * Processes the form. + */ + @Override + public void process(final FormSectionEvent event) + throws FormProcessException { + + PageState ps = event.getPageState(); + + // Get super parent group. + String key = (String) m_groupTree.getSelectedKey(ps); + + final Group parentGroup = null; +// if (key != null) { +// BigDecimal parentID = new BigDecimal(key); +// +// try { +// parentGroup = new Group(parentID); +// } catch (DataObjectNotFoundException exc) { +// // Parent group does not exist. +// // This is normal behavior with the new group +// // been add with no parent. +// } +// } + + final Group group = new Group(); + + String name = (String) m_name.getValue(ps); + group.setName(name); + + // Workaround for bug #189720: there is no way to remove a + // Party's primary email address, so we set it directly to + // null if it's value on the form is null. +// InternetAddress email = (InternetAddress) m_email.getValue(ps); +// if (email != null) { +// group.setPrimaryEmail(new EmailAddress(email.getAddress())); +// } else { +// //group.set("primaryEmail", null); +// group.setPrimaryEmail(null); +// } + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + groupRepository.save(group); + +// if (parentGroup != null) { +// parentGroup.addSubgroup(group); +// parentGroup.save(); +// } + if (m_groupTab != null) { + m_groupTab.setGroup(ps, group); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAdministrationTab.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAdministrationTab.java new file mode 100644 index 000000000..713e1253c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupAdministrationTab.java @@ -0,0 +1,685 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.ActionLink; +import com.arsdigita.bebop.BoxPanel; +import com.arsdigita.bebop.ColumnPanel; +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.ControlLink; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.List; +import com.arsdigita.bebop.Page; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.RequestLocal; +import com.arsdigita.bebop.SegmentedPanel; +import com.arsdigita.bebop.Tree; +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.bebop.event.ChangeEvent; +import com.arsdigita.bebop.event.ChangeListener; +import com.arsdigita.bebop.event.PrintEvent; +import com.arsdigita.bebop.event.PrintListener; +import com.arsdigita.bebop.list.ListCellRenderer; +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.toolbox.ui.LayoutPanel; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +import com.arsdigita.util.LockableImpl; +import com.arsdigita.util.UncheckedWrapperException; + +import java.math.BigDecimal; +import java.util.ArrayList; + +import org.apache.log4j.Logger; +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupRepository; + +/** + * Constructs the panel for administration of groups. + * + * @author David Dao + * + */ +class GroupAdministrationTab extends LayoutPanel implements AdminConstants, + ChangeListener { + + private static final Logger LOGGER = Logger.getLogger( + GroupAdministrationTab.class); + private final Tree groupTree; + private SearchAndList subMemberSearch; + private ActionLink addSubmemberLink; + private final Component groupInfoPanel; + private final Component subGroupPanel; + private final Component subMemberPanel; + private final Component extremeActionPanel; + private final Component groupAddPanel; + private final Component groupEditPanel; + private final Component groupDeleteFailedPanel; + private final Component existingGroupAddPanel; + private ExistingGroupAddPane m_existingGroupAdd; + private final java.util.List panelList + = new ArrayList(); + private final RequestLocal requestLocalGroup; + + /** + * + * @param page + */ + @Override + public void register(final Page page) { + for (int i = 0; i < panelList.size(); i++) { + page.setVisibleDefault(panelList.get(i), false); + } + + page.setVisibleDefault(groupAddPanel, true); + } + + /** + * + * @param state + * + * @return + */ + public Group getGroup(final PageState state) { + return (Group) requestLocalGroup.get(state); + } + + public void setGroup(final PageState state, final Group group) { + final String groupId = Long.toString(group.getSubjectId()); + requestLocalGroup.set(state, group); + groupTree.setSelectedKey(state, groupId); + + if (!"-1".equals(groupId)) { + expandGroups(state, group); + groupTree.expand("-1", state); + } + } + + private void expandGroups(final PageState state, final Group group) { +// groupTree.expand(Long.toString(group.getSubjectId()), state); +// +// final List< superGroups = group.getSupergroups(); +// Group superGroup; +// while (superGroups.next()) { +// superGroup = (Group) superGroups.getDomainObject(); +// expandGroups(state, superGroup); +// } + } + + /** + * Constructor + */ + public GroupAdministrationTab() { + super(); + + setClassAttr("sidebarNavPanel"); + setAttribute("navbar-title", "Groups"); + + requestLocalGroup = new RequestLocal() { + + @Override + protected Object initialValue(final PageState state) { + String key = (String) groupTree.getSelectedKey(state); + + Group group; + if (key != null) { + final long id = Long.parseLong(key); + + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil + .findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + + group = groupRepository.findById(id); + + return group; + } + return null; + } + + }; + + setClassAttr("navbar"); + + groupTree = new Tree(new GroupTreeModelBuilder()); + groupTree.addChangeListener(this); + setLeft(groupTree); + + final SegmentedPanel body = new SegmentedPanel(); + body.setClassAttr("main"); + + groupInfoPanel = buildGroupInfoPanel(body); + panelList.add(groupInfoPanel); + + groupEditPanel = buildGroupEditPanel(body); + panelList.add(groupEditPanel); + + subGroupPanel = buildSubGroupPanel(body); + panelList.add(subGroupPanel); + + groupAddPanel = buildGroupAddPanel(body); + panelList.add(groupAddPanel); + + existingGroupAddPanel = buildExistingGroupAddPanel(body); + panelList.add(existingGroupAddPanel); + + subMemberPanel = buildMemberListPanel(body); + panelList.add(subMemberPanel); + + extremeActionPanel = buildExtremeActionPanel(body); + panelList.add(extremeActionPanel); + + groupDeleteFailedPanel = buildGroupDeleteFailedPanel(body); + panelList.add(groupDeleteFailedPanel); + + setBody(body); + } + + public void displayAddGroupPanel(final PageState state) { + hideAll(state); + groupAddPanel.setVisible(state, true); + } + + private void displayAddExistingGroupPanel(final PageState state) { + hideAll(state); + existingGroupAddPanel.setVisible(state, true); + } + + public void displayEditPanel(final PageState state) { + hideAll(state); + groupEditPanel.setVisible(state, true); + } + + public void displayGroupInfoPanel(final PageState state) { + showAll(state); + groupEditPanel.setVisible(state, false); + groupAddPanel.setVisible(state, false); + groupDeleteFailedPanel.setVisible(state, false); + existingGroupAddPanel.setVisible(state, false); + } + + public void displayDeleteFailedPanel(final PageState state) { + hideAll(state); + groupDeleteFailedPanel.setVisible(state, true); + } + + /** + * + * @param event + */ + public void stateChanged(final ChangeEvent event) { + + final PageState ps = event.getPageState(); + final String key = (String) groupTree.getSelectedKey(ps); + // added cg - reset existing group add panel to the search screen + // when a new group is selected from the tree + m_existingGroupAdd.showSearch(ps); + if (key == null || key.equals("-1")) { + /** + * If root node is selected then display add panel only. + */ + displayAddGroupPanel(ps); + } else { + displayGroupInfoPanel(ps); + } + ps.setValue(GROUP_ID_PARAM, new BigDecimal(key)); + } + + /** + * Build a panel to display group basic information. + */ + private Component buildGroupInfoPanel(final SegmentedPanel main) { + final BoxPanel body = new BoxPanel(); + + //body.add(new GroupInfo(this)); + final ColumnPanel infoPanel = new ColumnPanel(2); + + infoPanel.add(new Label(new GlobalizedMessage("ui.admin.groups.name", + BUNDLE_NAME))); + final Label nameLabel = new Label(); + nameLabel.addPrintListener(new PrintListener() { + + @Override + public void prepare(final PrintEvent event) { + final Label target = (Label) event.getTarget(); + final PageState state = event.getPageState(); + final Group group = getGroup(state); + + target.setLabel(group.getName()); + } + + }); + infoPanel.add(nameLabel); + body.add(infoPanel); + + ActionLink link = new ActionLink(EDIT_GROUP_LABEL); + link.setClassAttr("actionLink"); + link.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + PageState ps = e.getPageState(); + displayEditPanel(ps); + } + + }); + body.add(link); + + return main.addSegment(GROUP_INFORMATION_HEADER, body); + + } + + /** + * Build group edit form. + */ + private Component buildGroupEditPanel(final SegmentedPanel main) { + return main.addSegment(GROUP_EDIT_HEADER, new GroupEditForm(this)); + } + + /** + * Build panel to display direct subgroup information. + */ + private Component buildSubGroupPanel(final SegmentedPanel main) { + final BoxPanel body = new BoxPanel(); + final BoxPanel labelStatus = new BoxPanel(BoxPanel.HORIZONTAL); + labelStatus.add(SUBGROUP_COUNT_LABEL); + + final Label countLabel = new Label(""); + countLabel.addPrintListener(new PrintListener() { + + @Override + public void prepare(final PrintEvent event) { +// final PageState ps = event.getPageState(); +// +// final Label target = (Label) event.getTarget(); +// Group g = getGroup(ps); +// if (g != null) { +// target.setLabel(String.valueOf(g.countSubgroups())); +// } + } + + }); + + final ActionLink status = new ActionLink(countLabel); + status.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(final ActionEvent event) { + PageState ps = event.getPageState(); + String key = (String) groupTree.getSelectedKey(ps); + groupTree.expand(key, ps); + } + + }); + labelStatus.add(status); + + body.add(labelStatus); + + final List subGroupList = new List(new SubGroupListModelBuilder(this)); + subGroupList.setCellRenderer(new ListCellRenderer() { + + @Override + public Component getComponent(final List list, + final PageState state, + final Object value, + final String key, + final int index, + final boolean isSelected) { + final BoxPanel b = new BoxPanel(BoxPanel.HORIZONTAL); + b.add(new Label(((Group) value).getName())); + final ControlLink removeLink = new ControlLink( + REMOVE_SUBGROUP_LABEL); + removeLink.setClassAttr("actionLink"); + b.add(removeLink); + return b; + } + + }); + + subGroupList.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(final ActionEvent event) { + final PageState state = event.getPageState(); + final String key = (String) ((List) event.getSource()) + .getSelectedKey(state); + + if (key != null) { + final Long groupId = Long.parseLong(key); + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil + .findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + + final Group group = groupRepository.findById(groupId); + final Group parent = getGroup(state); + if (parent != null) { + groupRepository.save(parent); + } + + final BigDecimal groupID = new BigDecimal(key); +// try { +// final Group group = new Group(groupID); +// final Group parent = getGroup(state); +// if (parent != null) { +// parent.removeSubgroup(group); +// parent.save(); +// } +// } catch (DataObjectNotFoundException exc) { +// } + } + } + + }); + body.add(subGroupList); + + final ActionLink addLink = new ActionLink(ADD_SUBGROUP_LABEL); + addLink.setClassAttr("actionLink"); + addLink.addActionListener(new ActionListener() { + + public void actionPerformed(final ActionEvent event) { + PageState ps = event.getPageState(); + + displayAddGroupPanel(ps); + } + + }); + + body.add(addLink); + + // new actionlink and anonymous ActionListener class added cg + final ActionLink addExistingLink = new ActionLink( + ADD_EXISTING_GROUP_TO_SUBGROUPS_LABEL); + addExistingLink.setClassAttr("actionLink"); + addExistingLink.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(final ActionEvent event) { + LOGGER.debug("Add existing group link pressed"); + PageState ps = event.getPageState(); + displayAddExistingGroupPanel(ps); + } + + }); + + body.add(addExistingLink); + return main.addSegment(SUBGROUP_HEADER, body); + + } + + /** + * Build group add form. + */ + private Component buildGroupAddPanel(final SegmentedPanel main) { + + return main.addSegment(ADD_GROUP_LABEL, + new GroupAddForm(groupTree, this)); + } + + private Component buildExistingGroupAddPanel(final SegmentedPanel main) { + m_existingGroupAdd = new ExistingGroupAddPane(groupTree, this); + return main.addSegment(ADD_EXISTING_GROUP_TO_SUBGROUPS_LABEL, + m_existingGroupAdd); + } + + /** + * Build group's member panel. + */ + private Component buildMemberListPanel(final SegmentedPanel main) { + + BoxPanel body = new BoxPanel() { + + @Override + public void register(final Page page) { + page.setVisibleDefault(subMemberSearch, false); + } + + }; +// body.add(new SubMemberPanel(this)); +// +// addSubmemberLink = new ActionLink(ADD_SUBMEMBER_LABEL); +// addSubmemberLink.setClassAttr("actionLink"); +// addSubmemberLink.addActionListener(new ActionListener() { +// +// @Override +// public void actionPerformed(final ActionEvent event) { +// PageState ps = event.getPageState(); +// addSubmemberLink.setVisible(ps, false); +// subMemberSearch.setVisible(ps, true); +// } +// +// }); +// +// subMemberSearch = new SearchAndList("searchsubmember"); +// subMemberSearch.setListModel(new UserSearchAndListModel()); +// subMemberSearch.addChangeListener(new ChangeListener() { +// +// @Override +// public void stateChanged(final ChangeEvent event) { +// PageState ps = event.getPageState(); +// +// String key = (String) subMemberSearch.getSelectedKey(ps); +// if (key != null) { +// final BigDecimal userID = new BigDecimal(key); +// +// final Group group = getGroup(ps); +// +// if (group != null) { +// try { +// User user = User.retrieve(userID); +// group.addMember(user); +// group.save(); +// } catch (DataObjectNotFoundException exc) { +// // Ignore if user id is not valid +// } catch (PersistenceException pexc) { +// // Display error message that user +// // already existed in group. +// } +// } +// } +// subMemberSearch.reset(ps); +// subMemberSearch.setVisible(ps, false); +// addSubmemberLink.setVisible(ps, true); +// } +// +// }); +// +// body.add(subMemberSearch); +// body.add(addSubmemberLink); + return main.addSegment(SUBMEMBER_HEADER, body); + + } + + /** + * Build extreme action panel. + */ + private Component buildExtremeActionPanel(final SegmentedPanel main) { + final BoxPanel body = new BoxPanel(); + + final ActionLink deleteLink = new ActionLink(DELETE_GROUP_LABEL); + deleteLink.setClassAttr("actionLink"); + deleteLink.setConfirmation(GROUP_DELETE_CONFIRMATION); + deleteLink.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(final ActionEvent event) { + + PageState ps = event.getPageState(); + + final Group group = (Group) requestLocalGroup.get(ps); + if (group != null) { + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch(CdiLookupException ex) { + throw new UncheckedWrapperException(ex); + } + + groupRepository.delete(group); + +// try { +// group.delete(); +// groupTree.setSelectedKey(ps, "-1"); +// } catch (PersistenceException exc) { +// LOGGER.warn("Error deleting subgroup", exc); +// displayDeleteFailedPanel(ps); +// } + } + // Select root node + + } + + }); + body.add(deleteLink); + return main.addSegment(GROUP_EXTREME_ACTIONS_HEADER, + body); + + } + + /** + * Build a panel to display an error message when unable to delete group. + */ + private Component buildGroupDeleteFailedPanel(final SegmentedPanel main) { + final ActionLink link = new ActionLink(GROUP_ACTION_CONTINUE); + link.addActionListener(new ActionListener() { + + public void actionPerformed(final ActionEvent event) { + PageState ps = event.getPageState(); + displayGroupInfoPanel(ps); + } + + }); + link.setClassAttr("actionLink"); + + final Label label = new Label(GROUP_DELETE_FAILED_MSG); + label.setClassAttr("deleteFailedMessage"); + + final BoxPanel panel = new BoxPanel(); + panel.add(label); + panel.add(link); + + return main.addSegment(GROUP_DELETE_FAILED_HEADER, panel); + } + + /** + * Hides all components of the in preparation for turning selected + * components back on. + */ + private void hideAll(final PageState state) { + for (int i = 0; i < panelList.size(); i++) { + ((Component) panelList.get(i)).setVisible(state, false); + } + } + + /** + * Show all components of the in preparation for turning visibility of + * selected components off . + */ + private void showAll(final PageState state) { + for (int i = 0; i < panelList.size(); i++) { + ((Component) panelList.get(i)).setVisible(state, true); + } + } + +} + +class SubGroupListModelBuilder extends LockableImpl implements ListModelBuilder { + + private final GroupAdministrationTab parent; + + public SubGroupListModelBuilder(final GroupAdministrationTab parent) { + this.parent = parent; + } + + public ListModel makeModel(final List list, final PageState state) { + final Group group = parent.getGroup(state); + +// if (group != null) { +// return new SubGroupListModel(group.getSubgroups()); +// } + + return new SubGroupListModel(null); + } + +} + +/** + * CLASS + * + */ +class SubGroupListModel implements ListModel { + +// private GroupCollection m_coll; + + /** + * + * @param collection + */ + public SubGroupListModel(final Object collection) { +// m_coll = collection; +// m_coll.addOrder("lower(" + Group.DISPLAY_NAME + ") asc"); + } + + /** + * + * @return + */ + public Object getElement() { +// return m_coll.getGroup(); + return null; + } + + /** + * + * @return + */ + public String getKey() { +// return m_coll.getID().toString(); + return null; + } + + /** + * + * @return + */ + public boolean next() { +// if (m_coll != null) { +// return m_coll.next(); +// } + + return false; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupEditForm.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupEditForm.java new file mode 100644 index 000000000..3dd748832 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupEditForm.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.event.FormInitListener; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.PageState; + +import java.math.BigDecimal; + +import javax.mail.internet.InternetAddress; + +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupRepository; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +/** + * Edit group form. + * + * @author David Dao + * @version $Id$ + */ +class GroupEditForm extends GroupForm implements FormInitListener, + FormProcessListener { + + private GroupAdministrationTab m_parent; + + public GroupEditForm() { + this(null); + } + + public GroupEditForm(final GroupAdministrationTab parent) { + super(GROUP_FORM_EDIT); + addInitListener(this); + addProcessListener(this); + + m_parent = parent; + } + + /** + * Initializes form elements by retrieving their values from the database. + */ + @Override + public void init(final FormSectionEvent event) { + final PageState state = event.getPageState(); + final Long id = (Long) state.getValue(USER_ID_PARAM); + + if (id != null) { + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + + try { + groupRepository = cdiUtil.findBean( + GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + + final Group group = groupRepository.findById(id); + + m_name.setValue(state, group.getName()); + } + } + + /** + * Processes the form. + */ + @Override + public void process(final FormSectionEvent event) + throws FormProcessException { + + final PageState state = event.getPageState(); + final Long id = (Long) state.getValue(GROUP_ID_PARAM); + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + + if (id == null) { + throw new FormProcessException(GlobalizationUtil.globalize( + "ui.admin.groups.ID_is_null")); + } + + final Group group = groupRepository.findById(id); + if (group == null) { + throw new FormProcessException(GlobalizationUtil.globalize( + "ui.admin.groups.couldnt_find_specified_group")); + } + + + final String name = (String) m_name.getValue(state); + group.setName(name); + + groupRepository.save(group); + + if (m_parent != null) { + m_parent.displayGroupInfoPanel(state); + } + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupForm.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupForm.java new file mode 100644 index 000000000..8a57c8f62 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupForm.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + + +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.form.TextField; +import com.arsdigita.bebop.form.Submit; +import com.arsdigita.bebop.parameters.EmailParameter; +import com.arsdigita.bebop.parameters.NotEmptyValidationListener; +import com.arsdigita.bebop.parameters.StringLengthValidationListener; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.bebop.ColumnPanel; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +/** + * Base form for add/edit group. + * + * @author David Dao + * @version $Id$ + */ +class GroupForm extends Form { + + protected TextField m_name; +// protected TextField m_email; + + public GroupForm(String formName) { + super(formName); + + m_name = new TextField(new StringParameter(GROUP_FORM_INPUT_NAME)); + m_name.setMaxLength(200); + m_name.addValidationListener(new NotEmptyValidationListener()); + m_name.addValidationListener(new StringLengthValidationListener (200)); + + + add(GROUP_FORM_LABEL_NAME); + add(m_name); + +// m_email = new TextField(new EmailParameter(GROUP_FORM_INPUT_PRIMARY_EMAIL)); +// m_email.setMaxLength(100); +// add(GROUP_FORM_LABEL_PRIMARY_EMAIL); +// add(m_email); + + // Submit button + add(new Submit(GROUP_FORM_SUBMIT), ColumnPanel.CENTER | + ColumnPanel.FULL_WIDTH); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupSearchForm.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupSearchForm.java new file mode 100644 index 000000000..3e5b5bcf3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupSearchForm.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2006 Chris Gilbert. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import java.util.List; + +import org.apache.log4j.Logger; + +import com.arsdigita.bebop.ColumnPanel; +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.SimpleContainer; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.form.Submit; +import com.arsdigita.bebop.form.TextField; +import com.arsdigita.bebop.parameters.NotEmptyValidationListener; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.StringParameter; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupRepository; + +import java.util.Collections; + +/** + * @author cgyg9330 + * + * Search for groups to add as subgroups. Text entered by user is put into a + * groupName like '%......%' query, which also excludes problematic results + * (groups that are already supergroups or subgroups of the current group + * + */ +public class GroupSearchForm extends Form implements FormProcessListener, + AdminConstants { + + private ExistingGroupAddPane parentPane; + private TextField m_search; + private List results = null; + + private static final Logger s_log = Logger.getLogger(GroupSearchForm.class); + + public GroupSearchForm(ExistingGroupAddPane parent) { + super("SearchGroups", new SimpleContainer()); + + parentPane = parent; + setMethod(Form.POST); + + addProcessListener(this); + + add(GROUP_SEARCH_LABEL); + add(new Label(" ", false)); + + StringParameter searchParam = new StringParameter(SEARCH_QUERY); + m_search = new TextField(searchParam); + m_search.addValidationListener(new NotEmptyValidationListener()); + m_search.setSize(20); + add(m_search, ColumnPanel.RIGHT); + + Submit submit = new Submit("submit"); + submit.setButtonLabel(SEARCH_BUTTON); + add(submit, ColumnPanel.LEFT); + } + + @Override + public void process(final FormSectionEvent event) + throws FormProcessException { + PageState state = event.getPageState(); + + Group parent = parentPane.getParentGroup(state); + String search = (String) m_search.getValue(state); + + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + results = groupRepository.searchGroupByName(search); + + + if (results.isEmpty()) { + parentPane.showNoResults(state); + } else { + // put search string into Page + state.setValue(getSearchString(), m_search.getValue(state)); + parentPane.showGroups(state); + } + + } + + /** + * + * allow other classes to get hold of the results, to avoid constructing the + * same query in several places + * + * @return + */ + public List getResults() { + return Collections.unmodifiableList(results); + } + + private ParameterModel getSearchString() { + return parentPane.getSearchString(); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModel.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModel.java new file mode 100644 index 000000000..f8d81ba31 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModel.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.PageState; + +import com.arsdigita.bebop.tree.TreeModel; +import com.arsdigita.bebop.tree.TreeNode; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupRepository; + +import java.math.BigDecimal; + +import java.util.Iterator; +import java.util.List; + +/** + * + * + * @author David Dao + * + */ +public class GroupTreeModel implements TreeModel { + +// private class GroupIterator implements Iterator { +// +// private List< m_coll; +// +// public GroupIterator(ACSObjectCollection coll) { +// m_coll = coll; +// } +// +// public boolean hasNext() { +// return m_coll.next(); +// } +// +// public Object next() { +// return new GroupTreeNode(m_coll.getACSObject()); +// } +// +// public void remove() { +// throw new UnsupportedOperationException(); +// } +// +// } + /** + * Obtain the root folder of the tree + * + * @param state + * + * @return + */ + @Override + public TreeNode getRoot(final PageState state) { + return new RootTreeNode(); + + } + + /** + * Check whether a given node has children + * + * @param node + * @param state + * + * @return + */ + @Override + public boolean hasChildren(final TreeNode node, final PageState state) { + + if (node instanceof RootTreeNode) { + return true; + } else { + return false; + } + + } + + /** + * Get direct children in this node. + * + * @param node + * @param state + * + * @return + */ + @Override + public Iterator getChildren(final TreeNode node, + final PageState state) { + + if (node instanceof RootTreeNode) { + + final CdiUtil cdiUtil = new CdiUtil(); + final GroupRepository groupRepository; + try { + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch (CdiLookupException ex) { + throw new UncheckedWrapperException( + "Failed to lookup GroupRepository", ex); + } + final List groups = groupRepository.findAll(); + + return groups.iterator(); + } else { + return null; + } + } + +} + +class RootTreeNode implements TreeNode { + + @Override + public Object getKey() { + return "-1"; + } + + @Override + public Object getElement() { + return "/"; + } + +} + +class GroupTreeNode implements TreeNode { + + private String m_key; + private String m_name; + + public GroupTreeNode(Group group) { + m_key = Long.toString(group.getSubjectId()); + m_name = group.getName(); + } + + @Override + public Object getKey() { + return m_key; + } + + @Override + public Object getElement() { + return m_name; + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModelBuilder.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModelBuilder.java new file mode 100644 index 000000000..f35773434 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/GroupTreeModelBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2013 Jens Pelzetter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Tree; +import com.arsdigita.bebop.tree.TreeModel; +import com.arsdigita.bebop.tree.TreeModelBuilder; +import com.arsdigita.util.LockableImpl; + +/** + * + * @author Jens Pelzetter + * @version $Id$ + */ +public class GroupTreeModelBuilder extends LockableImpl implements TreeModelBuilder { + + public TreeModel makeModel(final Tree tree, final PageState state) { + tree.expand("-1", state); + return new GroupTreeModel(); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/PartyListModel.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/PartyListModel.java new file mode 100644 index 000000000..15bb039a2 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/PartyListModel.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.list.ListModel; + +import org.libreccm.core.Subject; + +import java.util.List; + + +/** + * + * @version $Id$ + */ +class PartyListModel implements ListModel { + + private final List m_parties; + private Subject m_currentParty = null; + private int index = 0; + + /** + * Constructor for the list + * Builds the list of party list model for parties + * + * @param partys the partyCollection + **/ + public PartyListModel(final List parties) { + m_parties = parties; + } + + /** + * Check whether is an another party + * + * @return true if another party exist, false otherwise + **/ + @Override + public boolean next() { + if (index < m_parties.size()) { + index++; + m_currentParty = m_parties.get(index); + return true; + } else { + return false; + } + } + + /** + * Returns the unqiue ID string for current Party + * + * @return the unqiue ID string for current Party + **/ + @Override + public String getKey() { + return Long.toString(m_currentParty.getSubjectId()); + } + + /** + * Returns the current Party + * + * @return the current Party + **/ + @Override + public Object getElement() { + return m_currentParty.getSubjectId(); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndList.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndList.java new file mode 100644 index 000000000..91b0ffe8c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndList.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.ActionLink; +import com.arsdigita.bebop.BoxPanel; +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.FormData; +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.List; +import com.arsdigita.bebop.Page; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Resettable; +import com.arsdigita.bebop.SimpleContainer; +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.bebop.event.FormProcessListener; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.form.Submit; +import com.arsdigita.bebop.form.TextField; +import com.arsdigita.bebop.list.ListCellRenderer; +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.bebop.parameters.NotNullValidationListener; +import com.arsdigita.bebop.parameters.ParameterModel; +import com.arsdigita.bebop.parameters.StringParameter; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.util.LockableImpl; +import com.arsdigita.bebop.event.ChangeListener; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +/** + * + * @author David Dao + */ +class SearchAndList extends SimpleContainer + implements AdminConstants, + Resettable { + + /** + * String catalog. + */ + private static final String FORM_INPUT_NAME = "query"; + + private static final GlobalizedMessage LABEL_SUBMIT = new GlobalizedMessage( + "ui.admin.searchAndList.submit", + BUNDLE_NAME); + private static final GlobalizedMessage SEARCH_AGAIN = new GlobalizedMessage( + "ui.admin.searchAndList.submitAgain", + BUNDLE_NAME); + + private Form m_searchForm; + + private List m_searchResultList; + + private ParameterModel m_queryModel; + + private SearchAndListModel m_listModel; + + private SimpleContainer m_searchResultContainer; + + private class SearchListModelBuilder extends LockableImpl + implements ListModelBuilder { + + public ListModel makeModel(List l, PageState state) { + return m_listModel; + } + + } + + private ListModelBuilder m_listModelBuilder = new SearchListModelBuilder(); + + private FormProcessListener m_formProcessListener + = new FormProcessListener() { + + public void process(FormSectionEvent e) throws FormProcessException { + + FormData data = e.getFormData(); + PageState state = e.getPageState(); + String query = (String) data.get(FORM_INPUT_NAME); + + boolean visible = false; + if (query == null || query.equals("")) { + visible = true; + } else { + visible = false; + } + + m_listModel.setQuery(query); + m_searchForm.setVisible(state, visible); + m_searchResultContainer.setVisible(state, !visible); + + } + + }; + + public SearchAndList(String name) { + super(); + + /** + * Create a search form. + */ + m_searchForm = new Form(name, new BoxPanel(BoxPanel.HORIZONTAL)); + m_queryModel = new StringParameter(FORM_INPUT_NAME); + TextField query = new TextField(m_queryModel); + query.addValidationListener(new NotNullValidationListener()); + m_searchForm.add(query); + m_searchForm.add(new Submit(LABEL_SUBMIT)); + m_searchForm.addProcessListener(m_formProcessListener); + add(m_searchForm); + + /** + * Create a search result container. + */ + m_searchResultContainer = new SimpleContainer(); + add(m_searchResultContainer); + + m_searchResultList = new List(); + m_searchResultList.setClassAttr("SearchResultList"); + m_searchResultList.setModelBuilder(m_listModelBuilder); + + ActionLink link = new ActionLink(new Label(SEARCH_AGAIN)); + link.setClassAttr("actionLink"); + link.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + PageState state = e.getPageState(); + m_searchForm.setVisible(state, true); + m_searchResultContainer.setVisible(state, false); + } + + }); + + m_searchResultContainer.add(m_searchResultList); + m_searchResultContainer.add(link); + } + + public void addChangeListener(ChangeListener l) { + m_searchResultList.addChangeListener(l); + } + + public Object getSelectedKey(PageState ps) { + return m_searchResultList.getSelectedKey(ps); + } + + public void clearSelection(PageState ps) { + m_searchResultList.clearSelection(ps); + } + + public void setListModel(SearchAndListModel model) { + m_listModel = model; + } + + public void setResultCellRenderer(ListCellRenderer r) { + m_searchResultList.setCellRenderer(r); + } + + public void register(Page p) { + p.setVisibleDefault(m_searchForm, true); + p.setVisibleDefault(m_searchResultContainer, false); + + } + + public void reset(PageState ps) { + m_searchResultContainer.setVisible(ps, false); + m_searchForm.setVisible(ps, true); + clearSelection(ps); + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndListModel.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndListModel.java new file mode 100644 index 000000000..8ca632e0f --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/SearchAndListModel.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.list.ListModel; + +/** + * + * + * @author David Dao + * @version $Id$ + */ + +public interface SearchAndListModel extends ListModel { + + /** + * Specify the user's search. + */ + + public void setQuery (String query); +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/SelectGroups.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/SelectGroups.java new file mode 100644 index 000000000..d6cd30700 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/SelectGroups.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2006 Chris Gilbert. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import java.math.BigDecimal; +import java.util.TooManyListenersException; + +import org.apache.log4j.Logger; + +import com.arsdigita.bebop.BoxPanel; +import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.FormData; +import com.arsdigita.bebop.FormProcessException; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.event.FormSectionEvent; +import com.arsdigita.bebop.event.FormSubmissionListener; +import com.arsdigita.bebop.event.PrintEvent; +import com.arsdigita.bebop.event.PrintListener; +import com.arsdigita.bebop.form.CheckboxGroup; +import com.arsdigita.bebop.form.Option; +import com.arsdigita.bebop.form.OptionGroup; +import com.arsdigita.bebop.form.Submit; + +import org.libreccm.core.Group; + +import java.util.List; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +/** + * @author cgyg9330 + * + * contains a form that allows the user to select desired groups to be added as + * subgroups to the currently selected group + * + * NB could be improved with cancel button - currently need to use browser back + * button + */ +public class SelectGroups { + + private static final Logger s_log = Logger.getLogger(SelectGroups.class); + private static final String GROUPS_CBG = "groups_cbg"; + private BoxPanel groupPanel; + private CheckboxGroup groups; + private Form form; + private Submit save; + private ExistingGroupAddPane parentPane; + private GroupSearchForm searchForm; + + public SelectGroups(ExistingGroupAddPane parent, GroupSearchForm searchForm) { + parentPane = parent; + makeForm(); + groupPanel = new BoxPanel(); + groupPanel.add(form); + this.searchForm = searchForm; + } + + /** + * Builds the form used to select groups. + */ + private void makeForm() { + form = new Form("ChooseGroups", new BoxPanel()); + form.setMethod(Form.POST); + form.addSubmissionListener(new AddGroupsSubmissionListener()); + form.add(PICK_GROUPS); + groups = new CheckboxGroup(GROUPS_CBG); + groups.setClassAttr("vertical"); + + try { + groups.addPrintListener(new GroupSearchPrintListener()); + } catch (TooManyListenersException e) { + throw new RuntimeException(e.getMessage()); + } + form.add(groups); + + save = new Submit("save", SAVE_BUTTON); + form.add(save); + } + + public BoxPanel getPanel() { + return groupPanel; + } + + /** + * + * FormSubmissionListener that sets selected groups to be subgroups of the + * currently selected group + * + */ + private class AddGroupsSubmissionListener + implements FormSubmissionListener { + + public void submitted(FormSectionEvent e) throws FormProcessException { + PageState state = e.getPageState(); + FormData data = e.getFormData(); + String[] selectedGroups = (String[]) data.get(GROUPS_CBG); + BigDecimal groupID = null; + Group child = null; +// if (selectedGroups != null) { +// Group parent = parentPane.getParentGroup(state); +// for (int i = 0; i < selectedGroups.length; i++) { +// groupID = new BigDecimal(selectedGroups[i]); +// try { +// child = new Group(groupID); +// } catch (DataObjectNotFoundException e2) { +// s_log.warn("Non existant Group " + child +// + " selected to be child of " + parent); +// } +// parent.addSubgroup(child); +// parent.save(); +// } +// } + + parentPane.showSearch(state); + parentPane.getParentPage().displayGroupInfoPanel(state); + + } + + } + + /** + * + * @author cgyg9330 + * + * Printlistener retrieves query results from the groupsearch form and uses + * them as the entries on the checkbox group + */ + private class GroupSearchPrintListener implements PrintListener { + + public void prepare(PrintEvent e) { + PageState state = e.getPageState(); + OptionGroup cbg = (CheckboxGroup) e.getTarget(); + + List results = searchForm.getResults(); + + String groupID; + String groupName; + Group child; + + for(Group group : results) { + child = group; + groupID = Long.toString(child.getSubjectId()); + groupName = child.getName(); + cbg.addOption(new Option(groupID, groupName)); + } + } + + } + +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/SubMemberPanel.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/SubMemberPanel.java new file mode 100644 index 000000000..c7e9d3781 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/SubMemberPanel.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.util.LockableImpl; +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.List; +import com.arsdigita.bebop.BoxPanel; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.list.ListCellRenderer; +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.ControlLink; +import com.arsdigita.bebop.event.ActionListener; +import com.arsdigita.bebop.event.ActionEvent; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.cdi.utils.CdiLookupException; +import org.libreccm.cdi.utils.CdiUtil; +import org.libreccm.core.Group; +import org.libreccm.core.GroupManager; +import org.libreccm.core.GroupMembership; +import org.libreccm.core.GroupRepository; +import org.libreccm.core.User; +import org.libreccm.core.UserRepository; + +import static com.arsdigita.ui.admin.AdminConstants.*; + + +/** + * + * + * @author David Dao + * + */ +class SubMemberPanel extends BoxPanel { + + private List m_memberList; + + + private GroupAdministrationTab m_mainTab; + + public SubMemberPanel(final GroupAdministrationTab tab) { + m_mainTab = tab; + m_memberList = new List(new SubMemberListModelBuilder(tab)); + m_memberList.setCellRenderer(new ListCellRenderer() { + @Override + public Component getComponent(final List list, + final PageState state, + final Object value, + final String key, + final int index, + final boolean isSelected) { + + final BoxPanel panel = new BoxPanel(BoxPanel.HORIZONTAL); + + Label label = new Label(((User) value).getScreenName()); + panel.add(label); + + ControlLink removeLink = new ControlLink(REMOVE_SUBMEMBER_LABEL); + removeLink.setClassAttr("actionLink"); + + panel.add(removeLink); + return panel; + } + }); + m_memberList.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent event) { + + final PageState state = event.getPageState(); + final String key = (String) m_memberList.getSelectedKey(state); + + if (key != null) { + final Long userID = new Long(key); + final CdiUtil cdiUtil = new CdiUtil(); + final UserRepository userRepository; + final GroupManager groupManager; + final GroupRepository groupRepository; + try { + userRepository = cdiUtil.findBean(UserRepository.class); + groupManager = cdiUtil.findBean(GroupManager.class); + groupRepository = cdiUtil.findBean(GroupRepository.class); + } catch(CdiLookupException ex) { + throw new UncheckedWrapperException(ex); + } + + final User user = userRepository.findById(userID); + final Group group = m_mainTab.getGroup(state); + if (group != null) { + groupManager.removeUserFromGroup(user, group); + groupRepository.save(group); + } + } + + } + }); + add(m_memberList); + } +} + +class SubMemberListModelBuilder extends LockableImpl + implements ListModelBuilder { + + private GroupAdministrationTab m_mainTab; + public SubMemberListModelBuilder(final GroupAdministrationTab tab) { + m_mainTab = tab; + } + + @Override + public ListModel makeModel(final List list, final PageState state) { + + final Group group = m_mainTab.getGroup(state); + final java.util.List members; + if (group == null) { + members = null; + } else { + members = group.getMembers(); + } + + return new SubMemberListModel(members); + + + } +} + +class SubMemberListModel implements ListModel { + + private final java.util.List members; + private int index; + + + public SubMemberListModel(final java.util.List members) { + this.members = members; + } + + @Override + public Object getElement() { + return members.get(index); + } + + @Override + public String getKey() { + return Long.toString(members.get(index).getMembershipId()); + } + + @Override + public boolean next() { + if (index < members.size()) { + index++; + return true; + } else { + return false; + } + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/UserAdministrationTab.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/UserAdministrationTab.java new file mode 100644 index 000000000..89bd0a122 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/UserAdministrationTab.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2013 Jens Pelzetter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.ui.admin; + +import com.arsdigita.bebop.BoxPanel; +import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.Label; +import com.arsdigita.bebop.List; +import com.arsdigita.bebop.Page; +import com.arsdigita.bebop.PageState; +import com.arsdigita.bebop.Resettable; +import com.arsdigita.bebop.TabbedPane; +import com.arsdigita.bebop.event.ChangeEvent; +import com.arsdigita.bebop.event.ChangeListener; +import com.arsdigita.bebop.list.ListModel; +import com.arsdigita.bebop.list.ListModelBuilder; +import com.arsdigita.globalization.GlobalizedMessage; +import com.arsdigita.toolbox.ui.LayoutPanel; + +import static com.arsdigita.ui.admin.AdminConstants.*; + +import com.arsdigita.util.Assert; +import com.arsdigita.util.LockableImpl; +import com.arsdigita.xml.Element; + +import java.util.ArrayList; + +/** + * + * @author Jens Pelzetter + * @version $Id$ + */ +class UserAdministrationTab extends LayoutPanel { + + private final List sections; + private final java.util.List components = new ArrayList(); + private final java.util.List