/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.objects.classes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.script.ScriptContext; import org.apache.commons.lang3.StringUtils; import org.apache.ecs.xhtml.option; import org.apache.ecs.xhtml.select; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.script.ScriptContextManager; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.internal.xml.XMLAttributeValueFilter; import com.xpn.xwiki.objects.BaseCollection; import com.xpn.xwiki.objects.BaseProperty; import com.xpn.xwiki.objects.ListProperty; import com.xpn.xwiki.objects.meta.PropertyMetaClass; import com.xpn.xwiki.web.Utils; /** * @version $Id: 24e3b1f08b900689e4b35849032c831ebff523d8 $ */ public class DBTreeListClass extends DBListClass { private static final String XCLASSNAME = "dbtreelist"; private static final Logger LOGGER = LoggerFactory.getLogger(DBTreeListClass.class); /** In-memory cache of the ordered tree values, to be used in case it is supposed to be cached. */ private List<ListItem> cachedDBTreeList; public DBTreeListClass(PropertyMetaClass wclass) { super(XCLASSNAME, "DB Tree List", wclass); } public DBTreeListClass() { this(null); } public String getParentField() { return getStringValue("parentField"); } public void setParentField(String parentField) { setStringValue("parentField", parentField); } /** * Get the ordered list of tree nodes that is currently cached, if any. * * @param context the current request context * @return the cached list, or {@code null} if not already cached */ protected List<ListItem> getCachedDBTreeList(XWikiContext context) { if (isCache()) { // If the property is supposed to be cached long term ({@link #isCache()}), then the list is cached in // memory in the current object return this.cachedDBTreeList; } else { // Otherwise, to avoid re-computing the tree in case it is requested several times during the same request, // it is cached in the request context. return (List<ListItem>) context.get(context.getWikiId() + ":" + getFieldFullName() + "-tree"); } } /** * Store the ordered list of tree nodes in a cache. * * @param cachedDBTreeList the list to cache * @param context the current request context */ protected void setCachedDBTreeList(List<ListItem> cachedDBTreeList, XWikiContext context) { if (isCache()) { // If the property is supposed to be cached long term ({@link #isCache()}), then the list is cached in // memory in the current object this.cachedDBTreeList = cachedDBTreeList; } else { // Otherwise, to avoid re-computing the tree in case it is requested several times during the same request, // it is cached in the request context. context.put(context.getWikiId() + ":" + getFieldFullName() + "-tree", cachedDBTreeList); } } public Map<String, List<ListItem>> getTreeMap(XWikiContext context) { List<ListItem> list = getDBList(context); Map<String, List<ListItem>> map = new HashMap<String, List<ListItem>>(); if ((list == null) || (list.size() == 0)) { return map; } // The root of the tree is considered to be the empty string. // Make sure that entries with invalid parents end up in the tree. // TODO: Detect cycles, as these also don't appear in the tree. List<String> validParents = this.getList(context); for (ListItem item : list) { if (validParents.contains(item.getParent())) { addToList(map, item.getParent(), item); } else { addToList(map, "", item); } } return map; } /** * Gets an ordered list of items in the tree. This is necessary to make sure children are coming right after their * parents. * * @param treemap the unordered list of tree nodes * @param map the mapping between a node name and its corresponding tree node * @param context the current request context * @return ordered list of {@code ListItem} tree nodes */ protected List<ListItem> getTreeList(Map<String, List<ListItem>> treemap, Map<String, ListItem> map, XWikiContext context) { List<ListItem> list = getCachedDBTreeList(context); if (list == null) { list = new ArrayList<ListItem>(); addToTreeList(list, treemap, map, "", context); setCachedDBTreeList(list, context); } return list; } protected void addToTreeList(List<ListItem> treelist, Map<String, List<ListItem>> treemap, Map<String, ListItem> map, String parent, XWikiContext context) { List<ListItem> list = treemap.get(parent); if (list != null) { for (ListItem item : list) { ListItem item2 = new ListItem(item.getId(), getDisplayValue(item.getId(), "", map, context), item.getParent()); treelist.add(item2); addToTreeList(treelist, treemap, map, item.getId(), context); } } } protected void addToList(Map<String, List<ListItem>> map, String key, ListItem item) { List<ListItem> list = map.get(key); if (list == null) { list = new ArrayList<ListItem>(); map.put(key, list); } list.add(item); } @Override public void displayView(StringBuffer buffer, String name, String prefix, BaseCollection object, XWikiContext context) { List<String> selectlist; BaseProperty prop = (BaseProperty) object.safeget(name); if (prop == null) { selectlist = new ArrayList<String>(); } else if (prop instanceof ListProperty) { selectlist = ((ListProperty) prop).getList(); } else { selectlist = new ArrayList<String>(); selectlist.add(String.valueOf(prop.getValue())); } String result = displayFlatView(selectlist, context); if (result.equals("")) { super.displayView(buffer, name, prefix, object, context); } else { buffer.append(result); } } @Override public void displayEdit(StringBuffer buffer, String name, String prefix, BaseCollection object, XWikiContext context) { BaseProperty prop = (BaseProperty) object.safeget(name); List<String> selectlist = toList(prop); if (isPicker()) { String result = displayTree(name, prefix, selectlist, "edit", context); if (result.equals("")) { displayTreeSelectEdit(buffer, name, prefix, object, context); } else { displayHidden(buffer, name, prefix, object, context); buffer.append(result); } } else { displayTreeSelectEdit(buffer, name, prefix, object, context); } } private String displayFlatView(List<String> selectlist, XWikiContext context) { Map<String, ListItem> map = getMap(context); Map<String, List<ListItem>> treemap = getTreeMap(context); List<ListItem> fullTreeList = getTreeList(treemap, map, context); List<List<ListItem>> resList = new ArrayList<List<ListItem>>(selectlist.size()); for (String item : selectlist) { List<ListItem> itemPath = getItemPath(item, fullTreeList, new ArrayList<ListItem>()); mergeItems(itemPath, resList); } return renderItemsList(resList); } protected String renderItemsList(List<List<ListItem>> resList) { StringBuffer buff = new StringBuffer(); for (int i = 0; i < resList.size(); i++) { List<ListItem> items = resList.get(i); for (int j = 0; j < items.size(); j++) { ListItem item = items.get(j); buff.append(item.getValue()); if (j < items.size() - 1) { buff.append(" > "); } } if (i < resList.size() - 1) { buff.append("<br />"); } } return buff.toString(); } private void mergeItems(List<ListItem> itemPath, List<List<ListItem>> resList) { if (itemPath == null || itemPath.size() == 0) { return; } for (int i = 0; i < resList.size(); i++) { List<ListItem> items = resList.get(i); if (items.size() < itemPath.size()) { ListItem item1 = items.get(items.size() - 1); ListItem item2 = itemPath.get(items.size() - 1); if (item1.equals(item2)) { resList.set(i, itemPath); return; } } else { ListItem item1 = items.get(itemPath.size() - 1); ListItem item2 = itemPath.get(itemPath.size() - 1); if (item1.equals(item2)) { return; } } } resList.add(itemPath); } private List<ListItem> getItemPath(String item, List<ListItem> treeList, ArrayList<ListItem> resList) { for (ListItem tmpItem : treeList) { if (item.equals(tmpItem.getId())) { if (tmpItem.getParent().length() > 0) { getItemPath(tmpItem.getParent(), treeList, resList); } resList.add(tmpItem); return resList; } } return null; } private String displayTree(String name, String prefix, List<String> selectlist, String mode, XWikiContext context) { ScriptContextManager scriptManager = Utils.getComponent(ScriptContextManager.class); ScriptContext scontext = scriptManager.getCurrentScriptContext(); Map<String, ListItem> map = getMap(context); Map<String, List<ListItem>> treemap = getTreeMap(context); scontext.setAttribute("selectlist", selectlist, ScriptContext.ENGINE_SCOPE); scontext.setAttribute("fieldname", prefix + name, ScriptContext.ENGINE_SCOPE); scontext.setAttribute("tree", map, ScriptContext.ENGINE_SCOPE); scontext.setAttribute("treelist", getTreeList(treemap, map, context), ScriptContext.ENGINE_SCOPE); scontext.setAttribute("treemap", treemap, ScriptContext.ENGINE_SCOPE); scontext.setAttribute("mode", mode, ScriptContext.ENGINE_SCOPE); return context.getWiki().parseTemplate("treeview.vm", context); } protected void addToSelect(select select, List<String> selectlist, Map<String, ListItem> map, Map<String, List<ListItem>> treemap, String parent, String level, XWikiContext context) { List<ListItem> list = treemap.get(parent); if (list != null) { for (ListItem item : list) { String display = level + getDisplayValue(item.getId(), "", map, context); option option = new option(display, item.getId()); option.addElement(display); if (selectlist.contains(item.getId())) { option.setSelected(true); } select.addElement(option); addToSelect(select, selectlist, map, treemap, item.getId(), level + "\u00A0", context); } } } protected void displayTreeSelectEdit(StringBuffer buffer, String name, String prefix, BaseCollection object, XWikiContext context) { select select = new select(prefix + name, 1); select.setAttributeFilter(new XMLAttributeValueFilter()); select.setMultiple(isMultiSelect()); select.setSize(getSize()); select.setName(prefix + name); select.setID(prefix + name); select.setDisabled(isDisabled()); Map<String, ListItem> map = getMap(context); Map<String, List<ListItem>> treemap = getTreeMap(context); List<String> selectlist; BaseProperty prop = (BaseProperty) object.safeget(name); if (prop == null) { selectlist = new ArrayList<String>(); } else if (prop instanceof ListProperty) { selectlist = ((ListProperty) prop).getList(); } else { selectlist = new ArrayList<String>(); selectlist.add(String.valueOf(prop.getValue())); } // Add options from Set addToSelect(select, selectlist, map, treemap, "", "", context); buffer.append(select.toString()); } /** * <p> * Computes the query corresponding to the current XProperty. The query is either manually specified by the XClass * creator in the <tt>sql</tt> field, or, if the query field is blank, constructed using the <tt>classname</tt>, * <tt>idField</tt>, <tt>valueField</tt> and <tt>parentField</tt> properties. The query is constructed according to * the following rules: * </p> * <ul> * <li>If no classname, id and value fields are selected, return a query that return no rows, as the parent is not * enough to make a query.</li> * <li>If no parent field is provided, use the document "parent" medatada.</li> * <li>If only the classname is provided, select all document names which have an object of that type, preserving * the hierarchy defined by the parent field.</li> * <li>If only one of id and value is provided, use it for both columns.</li> * <li>If no classname is provided, assume the fields are document properties.</li> * <li>If the document is not used at all, don't put it in the query.</li> * <li>If the object is not used at all, don't put it in the query.</li> * </ul> * <p> * The generated query always selects 3 columns, the first one is used as the stored value, the second one as the * displayed value, and the third one defines the "parent" of the current value. * </p> * * @param context The current {@link XWikiContext context}. * @return The HQL query corresponding to this property. */ @Override public String getQuery(XWikiContext context) { // First, get the hql query entered by the user. String sql = getSql(); // If the query field is blank, construct a query using the classname, idField, // valueField and parentField properties. if (StringUtils.isBlank(sql)) { if (context.getWiki().getHibernateStore() != null) { // Extract the 3 properties in non-null variables. String classname = StringUtils.defaultString(getClassname()); String idField = StringUtils.defaultString(getIdField()); String valueField = StringUtils.defaultString(getValueField()); String parentField = StringUtils.defaultString(getParentField()); // Check if the properties are specified or not. boolean hasClassname = !StringUtils.isBlank(classname); boolean hasIdField = !StringUtils.isBlank(idField); boolean hasValueField = !StringUtils.isBlank(valueField); boolean hasParentField = !StringUtils.isBlank(parentField); if (!(hasIdField || hasValueField)) { // If only the classname is specified, return a query that selects all the // document names which have an object of that type, and the hierarchy is // defined by the document "parent" property (unless a parent property is // specified). if (hasClassname) { sql = "select distinct doc.fullName, doc.fullName, " + (hasParentField ? parentField : "doc.parent") + " from XWikiDocument as doc, BaseObject as obj" + " where doc.fullName=obj.name and obj.className='" + classname + "'"; } else { // If none of the first 3 properties is specified, return a query that // always returns no rows (only with the parent field no query can be made) sql = DEFAULT_QUERY; } return sql; } // If only one of the id and value fields is specified, use it for both columns. if (!hasIdField && hasValueField) { idField = valueField; } else if (hasIdField && !hasValueField) { valueField = idField; } // If no parent field was specified, use the document "parent" metadata if (!hasParentField) { parentField = "doc.parent"; } // Check if the document and object are needed or not. // The object is needed if there is a classname, or if at least one of the selected // columns is an object property. boolean usesObj = hasClassname || idField.startsWith("obj.") || valueField.startsWith("obj.") || parentField.startsWith("obj."); // The document is needed if one of the selected columns is a document property, or // if there is no classname specified and at least one of the selected columns is // not an object property. boolean usesDoc = idField.startsWith("doc.") || valueField.startsWith("doc.") || parentField.startsWith("doc."); if ((!idField.startsWith("obj.") || !valueField.startsWith("obj.") || !parentField.startsWith("obj.")) && !hasClassname) { usesDoc = true; } // Build the query in this variable. StringBuffer select = new StringBuffer("select distinct "); // These will hold the components of the from and where parts of the query. ArrayList<String> fromStatements = new ArrayList<String>(); ArrayList<String> whereStatements = new ArrayList<String>(); // Add the document to the query only if it is needed. if (usesDoc) { fromStatements.add("XWikiDocument as doc"); if (usesObj) { whereStatements.add("doc.fullName=obj.name"); } } // Add the object to the query only if it is needed. if (usesObj) { fromStatements.add("BaseObject as obj"); if (hasClassname) { whereStatements.add("obj.className='" + classname + "'"); } } // Add the first column to the query. if (idField.startsWith("doc.") || idField.startsWith("obj.")) { select.append(idField); } else if (!hasClassname) { select.append("doc." + idField); } else { select.append("idprop.value"); fromStatements.add("StringProperty as idprop"); whereStatements.add("obj.id=idprop.id.id and idprop.id.name='" + idField + "'"); } // Add the second column to the query. if (valueField.startsWith("doc.") || valueField.startsWith("obj.")) { select.append(", ").append(valueField); } else if (!hasClassname) { select.append(", doc." + valueField); } else { if (valueField.equals(idField)) { select.append(", idprop.value"); } else { select.append(", valueprop.value"); fromStatements.add("StringProperty as valueprop"); whereStatements.add("obj.id=valueprop.id.id and valueprop.id.name='" + valueField + "'"); } } // Add the third column to the query. if (parentField.startsWith("doc.") || parentField.startsWith("obj.")) { select.append(", ").append(parentField); } else if (!hasClassname) { select.append(", doc." + parentField); } else { if (parentField.equals(idField)) { select.append(", idprop.value"); } else if (parentField.equals(valueField)) { select.append(", valueprop.value"); } else { select.append(", parentprop.value"); fromStatements.add("StringProperty as parentprop"); whereStatements.add("obj.id=parentprop.id.id and parentprop.id.name='" + parentField + "'"); } } // Let's create the complete query select.append(" from "); select.append(StringUtils.join(fromStatements.iterator(), ", ")); if (whereStatements.size() > 0) { select.append(" where "); select.append(StringUtils.join(whereStatements.iterator(), " and ")); } sql = select.toString(); } else { // TODO: query plugin impl. // We need to generate the right query for the query plugin } } // Parse the query, so that it can contain velocity scripts, for example to use the // current document name, or the current username. try { sql = context.getWiki().parseContent(sql, context); } catch (Exception e) { LOGGER.error("Failed to parse SQL script [" + sql + "]. Continuing with non-rendered script.", e); } return sql; } }