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:
+ *
+ *
+ * + * Typically, the body of each segment will be a {@link SimpleContainer} + * which contains many other components + *+ * ---------------------- + * Header 1 + * ---------------------- + * Body 1 + * More Body 1 + * Even more Body 1 + * ---------------------- + * Header 2 + * ---------------------- + * Body 2 + * More Body 2 + * Even more Body 2 + *
+ * The XML generated by this component looks something like this: + *
+ * + * @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+ * <bebop:segmentedPanel> + * <bebop:segment id="foo"> + * <bebop:segmentHeader> + * <aRandomHeaderComponent/> + * <anotherRandomHeaderComponent/> + * ... + * </bebop:segmentHeader> + * <bebop:segmentBody> + * <aRandomBodyComponent> + * <anotherRandomBodyComponent> + * ... + * </bebop:segmentBody> + * </bebop:segment> + * ... + * </bebop:segmentedPanel> + *
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: + *
+ */
+ 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
+ * <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>
+ *
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