Customizable object list.

git-svn-id: https://svn.libreccm.org/ccm/trunk@810 8810af33-2d31-482b-a856-94f89814c4df
master
jensp 2011-03-30 15:53:00 +00:00
parent a1a7e209dc
commit 1d9cd2d691
7 changed files with 805 additions and 37 deletions

View File

@ -0,0 +1,189 @@
package com.arsdigita.london.navigation.ui.object;
import com.arsdigita.xml.Element;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A filter for the {@link CustomizableObjectList}. The {@code CompareFilter}
* filters the object list using a provided value and a operator. Valid
* operators defined in the {@link Operators} enumeration.
*
* @author Jens Pelzetter
*/
public class CompareFilter implements Filter {
private static final String ALL = "--ALL--";
private final String property;
private final String label;
private final boolean allOption;
private final boolean allOptionIsDefault;
private final boolean propertyIsNumeric;
private Map<String, Option> options = new LinkedHashMap<String, Option>();
private String value;
protected CompareFilter(final String property,
final String label,
final boolean allOption,
final boolean allOptionIsDefault,
final boolean propertyIsNumeric) {
this.property = property;
this.label = label;
this.allOption = allOption;
this.allOptionIsDefault = allOptionIsDefault;
this.propertyIsNumeric = propertyIsNumeric;
}
@Override
public void setValue(final String value) {
this.value = value;
}
public CompareFilter addOption(final String label, final String value) {
return addOption(label, Operators.EQ, value);
}
public CompareFilter addOption(final String label,
final Operators operator,
final String value) {
Option option;
option = new Option(label, operator, value);
options.put(label, option);
return this;
}
@Override
public String getFilter() {
Option selectedOption;
StringBuffer filter;
if ((value == null) || value.isEmpty()) {
if (allOptionIsDefault) {
value = ALL;
} else {
value =
new ArrayList<Option>(options.values()).get(0).getLabel();
}
}
if (ALL.equals(value)) {
return "";
}
selectedOption = options.get(value);
if (selectedOption == null) {
throw new IllegalArgumentException(String.format(
"Unknown option '%s' selected for CompareFilter for property '%s'.",
value,
property));
}
filter = new StringBuffer();
filter.append(property);
switch (selectedOption.getOperator()) {
case EQ:
filter.append(" = ");
break;
case LT:
filter.append(" < ");
break;
case GT:
filter.append(" > ");
break;
case LTEQ:
filter.append(" <= ");
break;
case GTEQ:
filter.append(" >= ");
break;
}
if (propertyIsNumeric) {
filter.append(selectedOption.getValue());
} else {
filter.append('\'');
filter.append(selectedOption.getValue());
filter.append('\'');
}
return filter.toString();
}
@Override
public Element getXml() {
Element filter;
String selected;
filter = new Element("compareFilter");
if ((value == null) || value.isEmpty()) {
if (allOptionIsDefault) {
selected = ALL;
} else {
List<Option> optionsList =
new ArrayList<Option>(options.values());
selected = optionsList.get(0).getLabel();
}
} else {
selected = value;
}
filter.addAttribute("label", label);
filter.addAttribute("selected", selected);
if (allOption) {
Element option;
option = filter.newChildElement("option");
option.addAttribute(label, ALL);
}
Element option;
for (Map.Entry<String, Option> entry : options.entrySet()) {
option = filter.newChildElement("option");
option.addAttribute("label", entry.getValue().getLabel());
}
return filter;
}
public enum Operators {
EQ, //equal, '{@code =}'
LT, //less than, '{@code <}'
GT, //greater than, '{@code >}'
LTEQ, //less than or equal, '{@code <=}'
GTEQ //greater than or equal, '{@code >=}'
}
public class Option {
private final String label;
private final Operators operator;
private final String value;
public Option(final String label,
final Operators operator,
final String value) {
this.label = label;
this.operator = operator;
this.value = value;
}
public String getLabel() {
return label;
}
public Operators getOperator() {
return operator;
}
public String getValue() {
return value;
}
}
}

View File

@ -11,8 +11,6 @@
* Angelegt wurde Sie für die Auflistung der aktuellen News
* und Veranstalungen auf einer Navigationsseite.
*/
package com.arsdigita.london.navigation.ui.object;
import com.arsdigita.london.navigation.Navigation;
@ -31,42 +29,42 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A complex object list
*/
public class ComplexObjectList extends AbstractObjectList {
public static final String CUSTOM_NAME = "customName";
protected String m_customName = null;
protected String m_filter = null;
protected Map m_filterParameters = new HashMap();
protected Map<String, String> m_customAttributes =
new HashMap<String, String>();
private Map<String, String> m_customAttributes = new HashMap<String, String>();
public void setCustomName(String name) {
m_customName = name;
}
public String getCustomName() {
return m_customName;
}
/**
* Hinzufügen eines SQL-Filter zur Abfrage
* Verarbeitet einen boolschen Filter, der SQL-konform Formatiert ist.
* Siehe PostgreSQL-Handbuch zur where-Klausel
* @param sqlfilter
*/
public void setSQLFilter(String sqlfilter) {
m_filter = sqlfilter;
}
public void setParameter(String parameterName, Object value) {
m_filterParameters.put(parameterName, value);
}
public String getCustomAttribute(final String attribute) {
@ -76,53 +74,56 @@ public class ComplexObjectList extends AbstractObjectList {
public void addCustomAttribute(final String attribute, final String value) {
m_customAttributes.put(attribute, value);
}
/* Diese Methode überschreibt die Methode aus der Eltern-Klasse, um
* die SQL-Filter berücksichtigen zu können
*/
/* Diese Methode überschreibt die Methode aus der Eltern-Klasse, um
* die SQL-Filter berücksichtigen zu können
*/
@Override
protected DataCollection getObjects( HttpServletRequest request, HttpServletResponse response ) {
DataCollection objects = super.getObjects( request, response );
protected DataCollection getObjects(HttpServletRequest request,
HttpServletResponse response) {
DataCollection objects = super.getObjects(request, response);
// Setze den Filter
if(m_filter != null) {
if (m_filter != null) {
FilterFactory fact = objects.getFilterFactory();
Filter sql = fact.simple(m_filter);
// Setze die Parameter
Iterator params = m_filterParameters.entrySet().iterator();
while(params.hasNext()) {
while (params.hasNext()) {
Map.Entry entry = (Map.Entry) params.next();
String param = (String) entry.getKey();
Object value = (Object) entry.getValue();
if(value != null) sql.set(param, value);
if (value != null) {
sql.set(param, value);
}
}
objects.addFilter(sql);
}
return objects;
}
/* Diese Methode wird vom Servlet aufgerufen */
public Element generateXML(HttpServletRequest request, HttpServletResponse response) {
public Element generateXML(HttpServletRequest request,
HttpServletResponse response) {
Element content = Navigation.newElement("complexObjectList");
if (m_customName != null) {
content.addAttribute(CUSTOM_NAME, m_customName);
}
for(Map.Entry<String, String> attribute : m_customAttributes.entrySet()) {
for (Map.Entry<String, String> attribute : m_customAttributes.entrySet()) {
content.addAttribute(attribute.getKey(), attribute.getValue());
}
content.addContent(generateObjectListXML(request, response));
return content;
}
}

View File

@ -0,0 +1,324 @@
package com.arsdigita.london.navigation.ui.object;
import com.arsdigita.london.navigation.Navigation;
import com.arsdigita.persistence.DataCollection;
import com.arsdigita.xml.Element;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
/**
* <p>
* An object list variant which can be filtered and sorted by the visitor of
* the website. The available filters and sort options are added in a JSP
* template. There are three kinds of filters yet:
* </p>
* <dl>
* <dt><code>TextFilter</code></dt>
* <dd>This filter filters the object list using a user provided string, which
* is put into the <code>WHERE</code> clause with <code>LIKE</code> operator.
* You might use this filter to allow the visitor to filter an object list for
* items with a specific name.</dd>
* <dt><code>SelectFilter</code></dt>
* <dd>This filter traverses through the objects displayed by the list and
* determines all distinct values of a property. The visitor can choose one
* of this values, and the displayed list will only contain items which where
* the property has the selected value.</dd>
* <dt><code>CompareFilter</code></dt>
* <dd>This filter also provides selectable options. But these options
* can be configured by the developer in the template.</dd>
* </dl>
* <p>
* If there is more than one filter, the values of all filters are combined
* using <code>AND</code>.
* </p>
* <p>
* This object list class was developed for displaying list of items from
* the Sci modules (SciPublications and SciOrganization). For example, we use
* this list to provide lists of publications which be filtered for publications
* from a specific year, for a specific author and for a specific title. The
* list can be sorted by the titles of the publications, the years of the
* publications and the (surnames of the) authors of the publications.
* </p>
* <p>
* As an example how to use this object list in a JSP template here are the
* relevant parts from the template for the publication list:
* </p>
* <pre>
* {@code
* ...
* <define:component name="itemList"
classname="com.arsdigita.london.navigation.ui.object.CustomizableObjectList"/>
* ...
* <jsp:scriptlet>
CustomizableObjectList objList = (CustomizableObjectList) itemList;
objList.setDefinition(new CMSDataCollectionDefinition());
objList.setRenderer(new CMSDataCollectionRenderer());
objList.getDefinition().setObjectType("com.arsdigita.cms.contenttypes.Publication");
objList.getDefinition().setDescendCategories(false);
objList.addTextFilter("title", "title");
objList.addTextFilter("authors.surname", "author");
objList.addSelectFilter("yearOfPublication", "year", true, true, true, true);
objList.addSortField("title", "title asc");
objList.addSortField("yearAsc", "yearOfPublication asc");
objList.addSortField("yearDesc", "yearOfPublication desc");
objList.addSortField("authors", "authors.surname asc, authors.givenname asc");
objList.getDefinition().addOrder(objList.getOrder(request.getParameter("sort")));
objList.getRenderer().setPageSize(20);
objList.getRenderer().setSpecializeObjects(true);
</jsp:scriptlet>
* ...
* }
* </pre>
* <p>
* You may notice the line
* <code>objList.getDefinition().addOrder(objList.getOrder(request.getParameter("sort")));</code>.
* This line may looks a bit weird to you. The reason is that it is not possible
* to access the <code>DataCollectionDefinition</code> from the methods in this
* class. If you try call the <code>addOrder()</code> from within this class
* you will cause an locking error.
* </p>
* @author Jens Pelzetter
* @version $Id$
* @see Filter
* @see TextFilter
* @see SelectFilter
* @see CompareFilter
*/
public class CustomizableObjectList extends ComplexObjectList {
private static final Logger logger = Logger.getLogger(
CustomizableObjectList.class);
/**
* The filters for the list. We use an {@link LinkedHashMap} here to
* preserve the insertation order.
*
*/
private final Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
/**
* The available sort fields. We use an {@link LinkedHashMap} here to
* preserve the insertation order.
*
*/
private final Map<String, String> sortFields = new LinkedHashMap<String, String>();
/**
* Sort by which property?
*/
private String sortBy = null;
/**
* Adds a new text filter to the list.
*
* @param property The property to filter using the new filter.
* @param label The label of the filter and its component.
* @see TextFilter#TextFilter(java.lang.String, java.lang.String)
*/
public void addTextFilter(final String property, final String label) {
TextFilter filter;
filter = new TextFilter(property, label);
filters.put(label, filter);
}
/**
* Adds a new compare filter to the list.
*
* @param property The property to filter using the new filter.
* @param label The label of the filter and its component.
* @param allOption Add an <em>all</em> option to the filter.
* @param allOptionIsDefault Is the all option the default?
* @param propertyIsNumeric Is the property to filter numeric?
* @return The new filter. Options can be added to the filter by calling
* the {@link CompareFilter#addOption(java.lang.String, java.lang.String)} or
* the {@link CompareFilter#addOption(java.lang.String, com.arsdigita.london.navigation.ui.object.CompareFilter.Operators, java.lang.String)}
* method.
* @see CompareFilter#CompareFilter(java.lang.String, java.lang.String, boolean, boolean, boolean)
*
*/
public CompareFilter addCompareFilter(final String property,
final String label,
final boolean allOption,
final boolean allOptionIsDefault,
final boolean propertyIsNumeric) {
CompareFilter filter;
filter = new CompareFilter(property,
label,
allOption,
allOptionIsDefault,
propertyIsNumeric);
filters.put(label, filter);
return filter;
}
/**
* Adds a select filter.
*
* @param property The property to filter.
* @param label The label of the filter.
* @param reverseOptions Reverse the order of the options.
* @param allOption Add an all option?
* @param allOptionIsDefault Is the all option the default.
* @param propertyIsNumeric Is the property numeric?
* @see SelectFilter#SelectFilter(java.lang.String, java.lang.String, com.arsdigita.london.navigation.ui.object.CustomizableObjectList, boolean, boolean, boolean, boolean)
*/
public void addSelectFilter(final String property,
final String label,
final boolean reverseOptions,
final boolean allOption,
final boolean allOptionIsDefault,
final boolean propertyIsNumeric) {
SelectFilter filter;
filter = new SelectFilter(property,
label,
this,
reverseOptions,
allOption,
allOptionIsDefault,
propertyIsNumeric);
filters.put(label, filter);
}
/**
* Add a sort field option.
*
* @param label The label of the sort field.
* @param property The property to sort by.
*/
public void addSortField(final String label, final String property) {
sortFields.put(label, property);
}
/**
* Determines which property is currently used for sorting.
*
* @param id The id of the sort field to sort by.
* @return The property to sort by.
*/
public String getOrder(final String id) {
String order = sortFields.get(id);
if ((order == null) || order.isEmpty()) {
return new ArrayList<String>(sortFields.values()).get(0);
}
return order;
}
/**
* This overwritten version of the <code>getObjects</code> method evaluates
* the parameters in HTTP request for the filters and creates an
* appropriate SQL filter and sets this filter.
*
* @param request
* @param response
* @return
*/
@Override
protected DataCollection getObjects(HttpServletRequest request,
HttpServletResponse response) {
//Set filters (using the SQL)
StringBuilder sqlFilters = new StringBuilder();
for (Map.Entry<String, Filter> filterEntry : filters.entrySet()) {
if ((filterEntry.getValue().getFilter() == null)
|| (filterEntry.getValue().getFilter().isEmpty())) {
continue;
}
if (sqlFilters.length() > 0) {
sqlFilters.append(" AND ");
}
sqlFilters.append(filterEntry.getValue().getFilter());
}
logger.debug(String.format("filters: %s", sqlFilters));
if (sqlFilters.length() > 0) {
setSQLFilter(sqlFilters.toString());
}
DataCollection objects = super.getObjects(request, response);
return objects;
}
/**
* <p>
* Generates the XML for the list. The root element for the list is
* <code>customizableObjectList</code>. The available filters are
* put into a <code>filters</code> element.
* </p>
* <p>
* The available sort fields are put into a <code>sortFields</code> element.
* This element has also an attribute indicating the current selected sort
* field.
* </p>
*
* @param request
* @param response
* @return
*/
@Override
public Element generateXML(HttpServletRequest request,
HttpServletResponse response) {
//Some stuff for the list (copied from ComplexObjectList)
Element content = Navigation.newElement("customizableObjectList");
if (m_customName != null) {
content.addAttribute(CUSTOM_NAME, m_customName);
}
for (Map.Entry<String, String> attribute : m_customAttributes.entrySet()) {
content.addAttribute(attribute.getKey(), attribute.getValue());
}
Element filterElems = content.newChildElement("filters");
for (Map.Entry<String, Filter> filterEntry : filters.entrySet()) {
filterElems.addContent(filterEntry.getValue().getXml());
}
//Look for values for the filters and the sort fields in the HTTP
//request. We are not using the Bebop parameters for two reasons:
//- They have to be registered very early, so we can't add new parameters
// from a JSP.
//- The HttpRequest is available here.
//So we use the HTTP request directly, which allows use to use a
//dedicated parameter for each of the filters.
for (Map.Entry<String, Filter> filterEntry : filters.entrySet()) {
String value = request.getParameter(filterEntry.getKey());
if ((value != null) && !value.isEmpty()) {
filterEntry.getValue().setValue(value);
}
}
//Look for a sort parameter. If one is found, use one to sort the data
//collection (if it is a valid value). If no sort parameter is found,
//use the first sort field as default.
String sortByKey = request.getParameter("sort");
sortBy = sortFields.get(sortByKey);
if (((sortBy == null)
|| sortBy.isEmpty()
|| !sortFields.containsKey(sortBy))
&& !sortFields.isEmpty()) {
sortByKey = new ArrayList<String>(sortFields.keySet()).get(0);
sortBy = new ArrayList<String>(sortFields.values()).get(0);
}
Element sortFieldElems = content.newChildElement("sortFields");
sortFieldElems.addAttribute("sortBy", sortByKey);
for (Map.Entry<String, String> sortField : sortFields.entrySet()) {
Element sortFieldElem = sortFieldElems.newChildElement("sortField");
sortFieldElem.addAttribute("label", sortField.getKey());
}
//Add object list
content.addContent(generateObjectListXML(request, response));
return content;
}
}

View File

@ -0,0 +1,34 @@
package com.arsdigita.london.navigation.ui.object;
import com.arsdigita.xml.Element;
/**
* Implementations of these interface are used by the
* {@link CustomizableObjectList} for filtering the objects in the list.
*
* @author Jens Pelzetter
* @version $Id$
* @see CustomizableObjectList
*/
interface Filter {
/**
*
* @return The SQL filter for filtering the object list.
*/
String getFilter();
/**
*
* @return XML representing the input component for the filter.
*/
Element getXml();
/**
* Used to set the value of the filter if the HTTP request contains a value
* for the filter.
*
* @param value The value from the input component.
*/
void setValue(String value);
}

View File

@ -0,0 +1,143 @@
package com.arsdigita.london.navigation.ui.object;
import com.arsdigita.persistence.DataCollection;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.xml.Element;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* This filter allows the user to filter the object list for a specific value
* of a property. The selectable values are determined by traversing
* through all objects of the list (before the list is processed by the
* paginator) and using each distinct value of the property as option.
*
* @author Jens Pelzetter
*/
public class SelectFilter implements Filter {
public static final String ALL = "--ALL--";
private final String property;
private final String label;
private final CustomizableObjectList objectList;
private final boolean allOption;
private final boolean allOptionIsDefault;
private final boolean reverseOptions;
private final boolean propertyIsNumeric;
//private Map<String, String> options = new HashMap<String, String>();
private String value;
protected SelectFilter(final String property,
final String label,
final CustomizableObjectList objectList,
final boolean reverseOptions,
final boolean allOption,
final boolean allOptionIsDefault,
final boolean propertyIsNumeric) {
this.property = property;
this.label = label;
this.objectList = objectList;
this.reverseOptions = reverseOptions;
this.allOption = allOption;
this.allOptionIsDefault = allOptionIsDefault;
this.propertyIsNumeric = propertyIsNumeric;
}
@Override
public void setValue(final String value) {
this.value = value;
}
@Override
public String getFilter() {
List<String> options;
options = getOptions();
if ((value == null) || value.isEmpty()) {
if (allOptionIsDefault) {
value = ALL;
} else {
value = options.get(0);
}
}
if (ALL.equals(value)) {
return null;
}
if (propertyIsNumeric) {
return String.format("%s = %s", property, value);
} else {
return String.format("%s = '%s'", property, value);
}
}
@Override
public Element getXml() {
Element filter;
Element optionElem;
String selected;
List<String> options;
options = getOptions();
filter = new Element("selectFilter");
if ((value == null) || value.isEmpty()) {
if (allOptionIsDefault) {
selected = ALL;
} else {
selected = options.get(0);
}
} else {
selected = value;
}
filter.addAttribute("label", label);
filter.addAttribute("selected", selected);
if (allOption) {
optionElem = filter.newChildElement("option");
optionElem.addAttribute(label, ALL);
}
for (String optionStr : options) {
optionElem = filter.newChildElement("option");
optionElem.addAttribute("label", optionStr);
}
return filter;
}
private List<String> getOptions() {
DataCollection objects;
DataObject dobj;
String option;
Set<String> optionsSet;
List<String> options;
objects = objectList.getDefinition().getDataCollection(objectList.
getModel());
optionsSet = new HashSet<String>();
while (objects.next()) {
dobj = objects.getDataObject();
option = (dobj.get(property)).toString();
optionsSet.add(option);
}
options = new ArrayList<String>(optionsSet);
Collections.sort(options);
if (reverseOptions) {
Collections.reverse(options);
}
return options;
}
}

View File

@ -0,0 +1,76 @@
package com.arsdigita.london.navigation.ui.object;
import com.arsdigita.xml.Element;
/**
* <p>
* Filter used by the {@link CustomizableObjectList}. These filter is usually
* rendered as a input box. The SQL filter created by this filter looks like
* this:
* </p>
* <p>
* {@code property LIKE 'value'}
* </p>
* @author Jens Pelzetter
*/
public class TextFilter implements Filter {
private final String property;
private final String label;
private String value;
/**
* Creates a new text filter. The constructor should only be invoked by the
* {@link CustomizableObjectList}.
*
* @param property The property which is used by this filter.
* @param label The label for the input component of the filter.
*/
protected TextFilter(final String property, final String label) {
this.property = property;
this.label = label;
}
@Override
public void setValue(final String value) {
this.value = value;
}
@Override
public String getFilter() {
if ((value == null) || value.isEmpty()) {
return null;
} else {
/*return String.format("(lower(%s) LIKE lower('%%%s%%'))",
property, value);*/
return String.format("(lower(%s) LIKE lower('%%%s%%')) OR (lower(%s) LIKE lower('%%%s%%'))",
property, value,
property, firstToUpper(value));
}
}
private String firstToUpper(final String str) {
char[] chars;
chars = str.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
return new String(chars);
}
@Override
public Element getXml() {
Element textFilter ;
textFilter = new Element("textFilter");
textFilter.addAttribute("label", label);
if ((value != null) && !value.isEmpty()) {
textFilter.addAttribute("value", value);
}
return textFilter;
}
}

View File

@ -7,6 +7,7 @@ waf.bebop.base_page=com.arsdigita.aplaws.ui.SimplePage
#waf.bebop.dhtml_editor=FCKeditor
waf.categorization.show_internal_name=true
waf.categorization.supported_languages=de,en
waf.dispatcher.default_expiry=3600
;