mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-07 17:49:17 +00:00
36311: BDE-69: filter long tests if minimal.testing property is defined 36314: Merged V4.0 to V4.0-BUG-FIX (RECORD ONLY) 36247: ALF-11027: temporarily remove import of maven.xml, since it makes ant calls fail from enterpriseprojects 36331: ALF-12447: Further changes required to fix lower case meta-inf folder name 36333: Revert ALF-12447. 36334: ALF-14115: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 36318: ALF-12447: Fix case on META-INF folder for SDK 36332: ALF-12447: Further changes required to fix lower case meta-inf folder name 36337: ALF-14115: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 36332: ALF-12447: Yet more meta-inf case changes needed. 36342: ALF-14120: fix only completed tasks returned 36343: ALF-13898: starting workflow from IMAP now using workflowDefs with engine name included, fallback to appending $jbpm when not present, to preserve backwards compatibility. 36345: Fix for ALF-12730 - Email Space Users fails if template is used 36346: Fix for ALF-9466 - We can search contents sorted by categories in Advanced search in Share, but saved search will not be shown in UI. 36364: Switch version to 4.0.3 36375: Merged BRANCHES/DEV/CLOUDSYNCLOCAL2 to BRANCHES/DEV/V4.0-BUG-FIX: 36366: Tweak to implementation to ensure that on-authentication-failed, the status is updated within a r/w transaction. 36374: Provide more specific exceptions from the Remote Connector Service for client and server errors 36376: Fix ALF-14121 - Alfresco fails to start if using "replicating-content-services-context.xml" 36393: Final part of ALF-13723 SOLR does not include the same query unit tests as lucene - CMIS typed query and ordering tests 36432: ALF-14133: Merged V3.4-BUG-FIX (3.4.10) to V4.0-BUG-FIX (4.0.3) << 4.0.x specific change: Changed transformer.complex.OOXML.Image into transformer.complex.Any.Image >> << allowing any transformer to be selected for the conversion to JPEG >> 36427: ALF-14131 Complex transformers fail if a lower level transformer fails even though there is another transformer that could do the transformation - Added a base spring bean for all complex transformers 36362: ALF-14131 Complex transformers fail if a lower level transformer fails even though there is another transformer that could do the transformation 36434: Test fix for ALF-13723 SOLR does not include the same query unit tests as lucene - CMIS test data change broke AFTS ID ordering 36503: Removed thousands of compiler warnings (CMIS query test code) 36518: Fix for ALF-13778 - Links on Share Repository search page show incorrect link name; do not work when root-node is defined. Fix now means that Share search correctly handles overridden Repository root node setting. Original work by Vasily Olhin. 36520: BDE-69: filter all repo tests if minimal.testing property is defined 36534: ALF-14116: Latest Surf libs (r1075) - ensure that i18n extensions can process browser sent short locales 36563: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 36336: ALF-12447: Yet more meta-inf case changes needed. 36347: Fix for ALF-13920 - Error occurred when try to edit/delete category 36352: Fix for ALF-13123 - Invalid JSON format from Get Node Tags Webscript - strings not double-quoted. Also fixed POST webscript with same issue. 36399: ALL LANG: translation updates based on EN r36392 36421: Fix for Mac Lion versioning issue. ALF-12792 (Part 1 of 2) Enable the InfoPassthru and Level2Oplocks server capability flags, InfoPassthru is the flag that fixes the Mac Lion versioning error. Added support for filesystems that do not implement the NTFS streams interface in the CIFS transact rename processing, for the Alfresco repo filesystem. 36422: Fix for Mac Lion versioning issue. ALF-12792 (Part 2 of 2) Enable the InfoPassthru and Level2Oplocks server capability flags, InfoPassthru is the flag that fixes the Mac Lion versioning error. 36423: Add support for file size tracking in the file state. ALF-13616 (Part 1 of 2) 36424: Fix for Mac MS Word file save issue. ALF-13616 (Part 2 of 2) Added live file size tracking to file writing/folder searches so the correct file size is returned before the file is closed. 36444: Merged DEV to V3.4-BUG-FIX 36419: ALF-12666 Search against simple-search-additional-attributes doesn't work properly SearchContext.buildQuery(int) method was changed. 36446: Fix for ALF-13404 - Performance: 'Content I'm Editing' dashlet is slow to render when there is lots of data/sites - Effectively removed all PATH based queries using the pattern /companyhome/sites/*/container//* as they are a non-optimized case - Replaced the "all sites" doclist query using the above pattern with /companyhome/sites//* plus post query resultset processing based on documentLibrary container matching regex - Optimized favorite document query to remove need for a PATH - Optimized Content I'm Editing discussion PATH query to use /*/* instead of /*//* - Fixed issue where Content I'm Editing discussion results would not always show the root topics that a user has edited - Added some addition doclist.get.js query scriptlogger debugging output 36449: ALF-13404 - Fix for issue where favoriates for all sites would be shown in each site document library in the My Favorites filter. 36475: ALF-14131 Complex transformers fail if a lower level transformer fails even though there is another transformer that could do the transformation - Change base spring bean on example config file 36480: 36453: ALF-3881 : ldap sync deletion behaviour not flexible enough - synchronization.allowDeletions parameter introduced - default value is true (existing behaviour) - when false, no missing users or groups are deleted from the repository - instead they are cleared of their zones and missing groups are cleared of all their members - colliding users and groups from different zones are also 'moved' rather than recreated - unit test added 36491: Added CIFS transact2 NT passthru levels for set end of file/set allocation size. ALF-13616. Also updated FileInfoLevel with the latest list of NT passthru information levels. 36497: Fixed ALF-14163: JavaScript Behaviour broken: Node properties cannot be cast to java.io.Serializable - Fallout from ALF-12855 - Made class Serializable (like HashMap would have been) - Fixed line endings, too 36531: ALF-13769: Merged BELARUS/V3.4-BUG-FIX-2012_04_05 to V3.4-BUG-FIX (3.4.10) 35150: ALF-2645 : 3.2+ ldap sync debug information is too scarce - Improved LDAP logging. 36532: ALF-13769: BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2012_01_26 to V3.4-BUG-FIX (3.4.10) 36461: ALF-237: WCM: File conflicts cause file order not to be consistent - It is reasonable set values for checkboxes using the indexes from the list, which are not changed. So when we submit the window, the getSelectedNodes method is invoked and it takes selected nodes by checkbox values from "paths" list. 36535: Merged DEV to V3.4-BUG-FIX 36479: ALF-8918 : Cannot "edit offline" a web quick start publication A check in TaggableAspect.onUpdatePropertiesOnCommit() was extended to skip the update, if no tags were changed. 36555: Merged V3.4 to V3.4-BUG-FIX 36294: ALF-14039: Merged HEAD to V3.4 31732: ALF-10934: Prevent potential start/stop ping-pong of subsystems across a cluster - When a cluster boots up or receives a reinit message it shouldn't be sending out any start messages 36566: Merged V3.4-BUG-FIX to V4.0-BUG-FIX (RECORD ONLY) 36172: Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/V3.4-BUG-FIX: 36169: ALF-8755: After renaming content / space by Contributor via WebDAV new items are created 36572: Merged V4.0 to V4.0-BUG-FIX 36388: ALF-14025: Updated Surf libs (1071). Fixes to checksum-disabled dependency handling 36392: ALF-14129 Failed to do upgrade from 3.4.8 to 4.0.2 << Committed change for Frederik Heremans >> - Moved actual activiti-tables creation to before the upgrade 36409: Fix for ALF-14124 Solr is not working - Errors occur during the startup 36466: Fix for ALF-12770 - Infinite loop popup alert in TinyMCE after XSS injection in Alfresco Explorer online edit. 36501: Merged DEV to V4.0 36496: ALF-14063 : CLONE - Internet Explorer hangs when using the object picker with a larger number of documents YUI 2.9.0 library was modified to use chunked unloading of listeners via a series of setTimeout() functions in event.js for IE 6,7,8. 36502: ALF-14105: Share Advanced search issue with the form values - Fix by David We 36538: ALF-13986: Updated web.xml and index.jsp redirect to ensure that SSO works with proper surf site-configuration customization 36539: Fix for ALF-14167 Filtering by Tags/Categories doen't findes any content in Repository/DocumentLibrary - fix default namespace back to "" -> "" and fix the specific SOLR tests that require otherwise. 36541: ALF-14082: Input stream leaks in thumbnail rendering webscripts 36560: Correctly size content length header after HTML stripping process (ALF-9365) 36574: Merged V4.0 to V4.0-BUG-FIX (RECORD ONLY) 36316: Merged V4.0-BUG-FIX to V4.0 (4.0.2) 36391: Merged V4.0-BUG-FIX to V4.0 36376: Fix ALF-14121 - Alfresco fails to start if using "replicating-content-services-context.xml" git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@36576 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
1039 lines
36 KiB
Java
1039 lines
36 KiB
Java
/*
|
|
* Copyright (C) 2005-2012 Alfresco Software Limited.
|
|
*
|
|
* This file is part of Alfresco
|
|
*
|
|
* Alfresco 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 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.alfresco.web.bean.search;
|
|
|
|
import java.io.Serializable;
|
|
import java.io.StringReader;
|
|
import java.io.StringWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.StringTokenizer;
|
|
|
|
import javax.faces.context.FacesContext;
|
|
|
|
import org.alfresco.error.AlfrescoRuntimeException;
|
|
import org.alfresco.model.ContentModel;
|
|
import org.alfresco.repo.search.impl.lucene.AbstractLuceneQueryParser;
|
|
import org.alfresco.service.cmr.repository.ChildAssociationRef;
|
|
import org.alfresco.service.cmr.repository.NodeRef;
|
|
import org.alfresco.service.cmr.repository.Path;
|
|
import org.alfresco.service.namespace.NamespaceService;
|
|
import org.alfresco.service.namespace.QName;
|
|
import org.alfresco.util.ISO9075;
|
|
import org.alfresco.web.bean.repository.Repository;
|
|
import org.apache.commons.logging.Log;
|
|
import org.apache.commons.logging.LogFactory;
|
|
import org.apache.lucene.queryParser.QueryParser;
|
|
import org.dom4j.Document;
|
|
import org.dom4j.DocumentHelper;
|
|
import org.dom4j.Element;
|
|
import org.dom4j.io.OutputFormat;
|
|
import org.dom4j.io.SAXReader;
|
|
import org.dom4j.io.XMLWriter;
|
|
|
|
/**
|
|
* Holds the context required to build a search query and can return the populated query.
|
|
* <p>
|
|
* Builds a lucene format search string from each of the supplied attributes and terms.
|
|
* Can be serialized to and from XML format for saving and restoring of previous searches.
|
|
*
|
|
* @author Kevin Roast
|
|
*/
|
|
public class SearchContext implements Serializable
|
|
{
|
|
private static final long serialVersionUID = 6730844584074229969L;
|
|
|
|
/** XML serialization elements */
|
|
private static final String ELEMENT_VALUE = "value";
|
|
private static final String ELEMENT_FIXED_VALUES = "fixed-values";
|
|
private static final String ELEMENT_INCLUSIVE = "inclusive";
|
|
private static final String ELEMENT_UPPER = "upper";
|
|
private static final String ELEMENT_LOWER = "lower";
|
|
private static final String ELEMENT_RANGE = "range";
|
|
private static final String ELEMENT_RANGES = "ranges";
|
|
private static final String ELEMENT_NAME = "name";
|
|
private static final String ELEMENT_ATTRIBUTE = "attribute";
|
|
private static final String ELEMENT_ATTRIBUTES = "attributes";
|
|
private static final String ELEMENT_MIMETYPE = "mimetype";
|
|
private static final String ELEMENT_CONTENT_TYPE = "content-type";
|
|
private static final String ELEMENT_FOLDER_TYPE = "folder-type";
|
|
private static final String ELEMENT_CATEGORY = "category";
|
|
private static final String ELEMENT_CATEGORIES = "categories";
|
|
private static final String ELEMENT_LOCATION = "location";
|
|
private static final String ELEMENT_MODE = "mode";
|
|
private static final String ELEMENT_TEXT = "text";
|
|
private static final String ELEMENT_SEARCH = "search";
|
|
private static final String ELEMENT_QUERY = "query";
|
|
|
|
/** advanced search term operators */
|
|
private static final char OP_WILDCARD = '*';
|
|
private static final char OP_AND = '+';
|
|
private static final char OP_NOT = '-';
|
|
private static final String STR_OP_WILDCARD = "" + OP_WILDCARD;
|
|
|
|
/** Search mode constants */
|
|
public final static int SEARCH_ALL = 0;
|
|
public final static int SEARCH_FILE_NAMES_CONTENTS = 1;
|
|
public final static int SEARCH_FILE_NAMES = 2;
|
|
public final static int SEARCH_SPACE_NAMES = 3;
|
|
|
|
/** the search text string */
|
|
private String text = "";
|
|
|
|
/** mode for the search */
|
|
private int mode = SearchContext.SEARCH_ALL;
|
|
|
|
/** folder XPath location for the search */
|
|
private String location = null;
|
|
|
|
/** categories to add to the search */
|
|
private String[] categories = new String[0];
|
|
|
|
/** folder type to restrict search against */
|
|
private String folderType = null;
|
|
|
|
/** content type to restrict search against */
|
|
private String contentType = null;
|
|
|
|
/** content mimetype to restrict search against */
|
|
private String mimeType = null;
|
|
|
|
/** any extra simple query attributes to add to the search */
|
|
protected List<QName> simpleSearchAdditionalAttrs = new ArrayList<QName>(4);
|
|
|
|
/** any extra query attributes to add to the search */
|
|
private Map<QName, String> queryAttributes = new HashMap<QName, String>(5, 1.0f);
|
|
|
|
/** any additional range attribute to add to the search */
|
|
private Map<QName, RangeProperties> rangeAttributes = new HashMap<QName, RangeProperties>(5, 1.0f);
|
|
|
|
/** any additional fixed value attributes to add to the search, such as boolean or noderef */
|
|
private Map<QName, String> queryFixedValues = new HashMap<QName, String>(5, 1.0f);
|
|
|
|
/** set true to force the use of AND between text terms */
|
|
private boolean forceAndTerms = false;
|
|
|
|
/** logger */
|
|
private static Log logger = LogFactory.getLog(SearchContext.class);
|
|
|
|
|
|
/**
|
|
* Build the search query string based on the current search context members.
|
|
*
|
|
* @param minimum small possible textual string used for a match
|
|
* this does not effect fixed values searches (e.g. boolean, int values) or date ranges
|
|
*
|
|
* @return prepared search query string
|
|
*/
|
|
public String buildQuery(int minimum)
|
|
{
|
|
String query;
|
|
boolean validQuery = false;
|
|
|
|
// the QName for the well known "name" attribute
|
|
String nameAttr = Repository.escapeQName(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, ELEMENT_NAME));
|
|
|
|
StringBuilder plBuf = new StringBuilder("(");
|
|
StringBuilder mnBuf = new StringBuilder("-(");
|
|
|
|
// match against content text
|
|
String text = this.text.trim();
|
|
|
|
if (text.length() != 0 && text.length() >= minimum)
|
|
{
|
|
if (text.indexOf(' ') == -1 && text.charAt(0) != '"')
|
|
{
|
|
// check for existance of a special operator
|
|
boolean operatorAND = (text.charAt(0) == OP_AND);
|
|
boolean operatorNOT = (text.charAt(0) == OP_NOT);
|
|
// strip operator from term if one was found
|
|
if (operatorAND || operatorNOT)
|
|
{
|
|
text = text.substring(1);
|
|
}
|
|
|
|
if (text.length() != 0)
|
|
{
|
|
// prepend NOT operator if supplied
|
|
if (operatorNOT)
|
|
{
|
|
processSearchTextAttribute(nameAttr, text, mode == SEARCH_FILE_NAMES_CONTENTS || mode == SEARCH_ALL, mnBuf);
|
|
}
|
|
|
|
// prepend AND operator if supplied
|
|
if (operatorAND)
|
|
{
|
|
processSearchTextAttribute(nameAttr, text, mode == SEARCH_FILE_NAMES_CONTENTS || mode == SEARCH_ALL, plBuf);
|
|
}
|
|
|
|
processSearchAdditionalAttribute(text, operatorNOT, operatorAND, mnBuf, plBuf, this.simpleSearchAdditionalAttrs);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// multiple word search
|
|
if (text.charAt(0) == '"' && text.charAt(text.length() - 1) == '"')
|
|
{
|
|
// as a single quoted phrase
|
|
String quotedSafeText = '"' + QueryParser.escape(text.substring(1, text.length() - 1)) + '"';
|
|
plBuf.append("TEXT:").append(quotedSafeText);
|
|
plBuf.append(" @").append(nameAttr).append(":").append(quotedSafeText);
|
|
for (QName qname : this.simpleSearchAdditionalAttrs)
|
|
{
|
|
plBuf.append(" @").append(
|
|
Repository.escapeQName(qname)).append(":").append(quotedSafeText);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// as individual search terms
|
|
StringTokenizer t = new StringTokenizer(text, " ");
|
|
|
|
int termCount = 0;
|
|
int tokenCount = t.countTokens();
|
|
for (int i=0; i<tokenCount; i++)
|
|
{
|
|
String term = t.nextToken();
|
|
|
|
// check for existance of a special operator
|
|
boolean operatorAND = (term.charAt(0) == OP_AND);
|
|
boolean operatorNOT = (term.charAt(0) == OP_NOT);
|
|
// strip operator from term if one was found
|
|
if (operatorAND || operatorNOT)
|
|
{
|
|
term = term.substring(1);
|
|
}
|
|
|
|
// special case for AND all terms if set (apply after operator character removed)
|
|
// note that we can't force AND if NOT operator has been set
|
|
if (operatorNOT == false)
|
|
{
|
|
operatorAND = operatorAND | this.forceAndTerms;
|
|
}
|
|
|
|
if (term.length() != 0)
|
|
{
|
|
// prepend NOT operator if supplied
|
|
if (operatorNOT)
|
|
{
|
|
processSearchTextAttribute(nameAttr, term, mode == SEARCH_FILE_NAMES_CONTENTS || mode == SEARCH_ALL, mnBuf);
|
|
}
|
|
|
|
// prepend AND operator if supplied
|
|
if (operatorAND)
|
|
{
|
|
processSearchTextAttribute(nameAttr, term, mode == SEARCH_FILE_NAMES_CONTENTS || mode == SEARCH_ALL, plBuf);
|
|
}
|
|
|
|
if (mode == SearchContext.SEARCH_ALL)
|
|
{
|
|
processSearchAdditionalAttribute(term, operatorNOT, operatorAND, mnBuf, plBuf, this.simpleSearchAdditionalAttrs);
|
|
}
|
|
|
|
termCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
plBuf.append(')');
|
|
mnBuf.append(')');
|
|
|
|
validQuery = true;
|
|
}
|
|
|
|
// match a specific PATH for space location or categories
|
|
StringBuilder pathQuery = null;
|
|
if (location != null || (categories != null && categories.length !=0))
|
|
{
|
|
pathQuery = new StringBuilder(128);
|
|
if (location != null)
|
|
{
|
|
pathQuery.append(" PATH:\"").append(location).append("\" ");
|
|
if (categories != null && categories.length != 0)
|
|
{
|
|
pathQuery.append("AND (");
|
|
}
|
|
}
|
|
if (categories != null && categories.length != 0)
|
|
{
|
|
for (int i=0; i<categories.length; i++)
|
|
{
|
|
pathQuery.append(" PATH:\"").append(categories[i]).append("\" ");
|
|
}
|
|
if (location != null)
|
|
{
|
|
pathQuery.append(") ");
|
|
}
|
|
}
|
|
}
|
|
|
|
// match any extra query attribute values specified
|
|
StringBuilder attributeQuery = null;
|
|
if (queryAttributes.size() != 0)
|
|
{
|
|
attributeQuery = new StringBuilder(queryAttributes.size() << 6);
|
|
for (QName qname : queryAttributes.keySet())
|
|
{
|
|
String value = queryAttributes.get(qname).trim();
|
|
if (value.length() >= minimum)
|
|
{
|
|
processSearchAttribute(qname, value, attributeQuery);
|
|
}
|
|
}
|
|
|
|
// handle the case where we did not add any attributes due to minimum length restrictions
|
|
if (attributeQuery.length() == 0)
|
|
{
|
|
attributeQuery = null;
|
|
}
|
|
}
|
|
|
|
// match any extra fixed value attributes specified
|
|
if (queryFixedValues.size() != 0)
|
|
{
|
|
if (attributeQuery == null)
|
|
{
|
|
attributeQuery = new StringBuilder(queryFixedValues.size() << 6);
|
|
}
|
|
for (QName qname : queryFixedValues.keySet())
|
|
{
|
|
String escapedName = Repository.escapeQName(qname);
|
|
String value = queryFixedValues.get(qname);
|
|
attributeQuery.append(" +@").append(escapedName)
|
|
.append(":\"").append(QueryParser.escape(value)).append('"');
|
|
}
|
|
}
|
|
|
|
// range attributes are a special case also
|
|
if (rangeAttributes.size() != 0)
|
|
{
|
|
if (attributeQuery == null)
|
|
{
|
|
attributeQuery = new StringBuilder(rangeAttributes.size() << 6);
|
|
}
|
|
for (QName qname : rangeAttributes.keySet())
|
|
{
|
|
String escapedName = Repository.escapeQName(qname);
|
|
RangeProperties rp = rangeAttributes.get(qname);
|
|
String value1 = QueryParser.escape(rp.lower);
|
|
String value2 = QueryParser.escape(rp.upper);
|
|
attributeQuery.append(" +@").append(escapedName)
|
|
.append(":").append(rp.inclusive ? "[" : "{").append(value1)
|
|
.append(" TO ").append(value2).append(rp.inclusive ? "]" : "}");
|
|
}
|
|
}
|
|
|
|
// mimetype is a special case - it is indexed as a special attribute it comes from the combined
|
|
// ContentData attribute of cm:content - ContentData string cannot be searched directly
|
|
if (mimeType != null && mimeType.length() != 0)
|
|
{
|
|
if (attributeQuery == null)
|
|
{
|
|
attributeQuery = new StringBuilder(64);
|
|
}
|
|
String escapedName = Repository.escapeQName(QName.createQName(ContentModel.PROP_CONTENT + ".mimetype"));
|
|
attributeQuery.append(" +@").append(escapedName)
|
|
.append(":").append(mimeType);
|
|
}
|
|
|
|
// match against appropriate content type
|
|
String fileTypeQuery;
|
|
if (contentType != null)
|
|
{
|
|
fileTypeQuery = " TYPE:\"" + contentType + "\" ";
|
|
}
|
|
else
|
|
{
|
|
// default to cm:content
|
|
fileTypeQuery = " TYPE:\"{" + NamespaceService.CONTENT_MODEL_1_0_URI + "}content\" ";
|
|
}
|
|
|
|
// match against appropriate folder type
|
|
String folderTypeQuery;
|
|
if (folderType != null)
|
|
{
|
|
folderTypeQuery = " TYPE:\"" + folderType + "\" ";
|
|
}
|
|
else
|
|
{
|
|
folderTypeQuery = " TYPE:\"{" + NamespaceService.CONTENT_MODEL_1_0_URI + "}folder\" ";
|
|
}
|
|
|
|
if (text.length() != 0 && text.length() >= minimum)
|
|
{
|
|
StringBuilder buf = new StringBuilder(128);
|
|
if (plBuf.length() > 2)
|
|
{
|
|
buf.append(plBuf);
|
|
if (mnBuf.length() > 3)
|
|
{
|
|
buf.append(" AND ");
|
|
}
|
|
}
|
|
if (mnBuf.length() > 3)
|
|
{
|
|
buf.append(mnBuf);
|
|
}
|
|
// text query for name and/or full text specified
|
|
switch (mode)
|
|
{
|
|
case SearchContext.SEARCH_ALL:
|
|
query = '(' + fileTypeQuery + " AND " + '(' + buf + ')' + ')' +
|
|
' ' +
|
|
'(' + folderTypeQuery + " AND " + '(' + buf + "))";
|
|
break;
|
|
|
|
case SearchContext.SEARCH_FILE_NAMES:
|
|
case SearchContext.SEARCH_FILE_NAMES_CONTENTS:
|
|
query = fileTypeQuery + " AND " + '(' + buf + ')';
|
|
break;
|
|
|
|
case SearchContext.SEARCH_SPACE_NAMES:
|
|
query = folderTypeQuery + " AND " + buf;
|
|
break;
|
|
|
|
default:
|
|
throw new IllegalStateException("Unknown search mode specified: " + mode);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no text query specified - must be an attribute/value query only
|
|
switch (mode)
|
|
{
|
|
case SearchContext.SEARCH_ALL:
|
|
query = '(' + fileTypeQuery + ' ' + folderTypeQuery + ')';
|
|
break;
|
|
|
|
case SearchContext.SEARCH_FILE_NAMES:
|
|
case SearchContext.SEARCH_FILE_NAMES_CONTENTS:
|
|
query = fileTypeQuery;
|
|
break;
|
|
|
|
case SearchContext.SEARCH_SPACE_NAMES:
|
|
query = folderTypeQuery;
|
|
break;
|
|
|
|
default:
|
|
throw new IllegalStateException("Unknown search mode specified: " + mode);
|
|
}
|
|
}
|
|
|
|
// match entire query against any additional attributes specified
|
|
if (attributeQuery != null)
|
|
{
|
|
query = attributeQuery + " AND (" + query + ')';
|
|
}
|
|
|
|
// match entire query against any specified paths
|
|
if (pathQuery != null)
|
|
{
|
|
query = "(" + pathQuery + ") AND (" + query + ')';
|
|
}
|
|
|
|
query = "(" + query + ") AND NOT ASPECT:\"sys:hidden\" ";
|
|
|
|
// check that we have a query worth executing - if we have no attributes, paths or text/name search
|
|
// then we'll only have a search against files/type TYPE which does nothing by itself!
|
|
validQuery = validQuery | (attributeQuery != null) | (pathQuery != null);
|
|
if (validQuery == false)
|
|
{
|
|
query = null;
|
|
}
|
|
|
|
if (logger.isDebugEnabled())
|
|
logger.debug("Query:\r\n" + query);
|
|
|
|
return query;
|
|
}
|
|
|
|
/**
|
|
* Build the lucene search terms required for the specified attribute and append to a buffer.
|
|
* Supports text values with a wildcard '*' character as the prefix and/or the suffix.
|
|
*
|
|
* @param qname QName of the attribute
|
|
* @param value Non-null value of the attribute
|
|
* @param buf Buffer to append lucene terms to
|
|
* @param andOp If true apply the '+' AND operator as the prefix to the attribute term
|
|
* @param notOp If true apply the '-' NOT operator as the prefix to the attribute term
|
|
*/
|
|
private static void processSearchAttribute(QName qname, String value, StringBuilder buf)
|
|
{
|
|
buf.append(" @").append(Repository.escapeQName(qname)).append(":\"")
|
|
.append(SearchContext.escape(value)).append("\" ");
|
|
}
|
|
|
|
/**
|
|
* Build the lucene search terms required for the specified attribute and append to multiple buffers.
|
|
* Supports text values with a wildcard '*' character as the prefix and/or the suffix.
|
|
*
|
|
* @param qname QName.toString() of the attribute
|
|
* @param value Non-null value of the attribute
|
|
* @param attrBuf Attribute search buffer to append lucene terms to
|
|
* @param textBuf Text search buffer to append lucene terms to
|
|
*/
|
|
private static void processSearchTextAttribute(String qname, String value, boolean appendText, StringBuilder mnBuf)
|
|
{
|
|
mnBuf.append('@').append(qname).append(":\"")
|
|
.append(SearchContext.escape(value)).append('"');
|
|
if (appendText)
|
|
{
|
|
mnBuf.append(" TEXT:\"").append(SearchContext.escape(value)).append("\" ");
|
|
}
|
|
}
|
|
|
|
private static void processSearchAdditionalAttribute(String value, boolean operatorNOT, boolean operatorAND, StringBuilder mnBuf, StringBuilder plBuf,
|
|
List<QName> simpleSearchAdditionalAttrs)
|
|
{
|
|
for (QName qname : simpleSearchAdditionalAttrs)
|
|
{
|
|
// prepend NOT operator if supplied
|
|
if (operatorNOT)
|
|
{
|
|
processSearchAttribute(qname, value, mnBuf);
|
|
}
|
|
|
|
// prepend AND operator if supplied
|
|
if (operatorAND)
|
|
{
|
|
processSearchAttribute(qname, value, plBuf);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a String where those characters that QueryParser
|
|
* expects to be escaped are escaped by a preceding <code>\</code>.
|
|
* '*' and '?' are not escaped.
|
|
*/
|
|
private static String escape(String s)
|
|
{
|
|
StringBuffer sb = new StringBuffer(s.length() + 4);
|
|
for (int i = 0; i < s.length(); i++)
|
|
{
|
|
char c = s.charAt(i);
|
|
if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' ||
|
|
c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~')
|
|
{
|
|
sb.append('\\');
|
|
}
|
|
sb.append(c);
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Generate a search XPATH pointing to the specified node, optionally return an XPATH
|
|
* that includes the child nodes.
|
|
*
|
|
* @param id Of the node to generate path too
|
|
* @param children Whether to include children of the node
|
|
*
|
|
* @return the path
|
|
*/
|
|
public static String getPathFromSpaceRef(NodeRef ref, boolean children)
|
|
{
|
|
FacesContext context = FacesContext.getCurrentInstance();
|
|
Path path = Repository.getServiceRegistry(context).getNodeService().getPath(ref);
|
|
NamespaceService ns = Repository.getServiceRegistry(context).getNamespaceService();
|
|
StringBuilder buf = new StringBuilder(64);
|
|
for (int i=0; i<path.size(); i++)
|
|
{
|
|
String elementString = "";
|
|
Path.Element element = path.get(i);
|
|
if (element instanceof Path.ChildAssocElement)
|
|
{
|
|
ChildAssociationRef elementRef = ((Path.ChildAssocElement)element).getRef();
|
|
if (elementRef.getParentRef() != null)
|
|
{
|
|
Collection<String> prefixes = ns.getPrefixes(elementRef.getQName().getNamespaceURI());
|
|
if (prefixes.size() >0)
|
|
{
|
|
elementString = '/' + (String)prefixes.iterator().next() + ':' + ISO9075.encode(elementRef.getQName().getLocalName());
|
|
}
|
|
}
|
|
}
|
|
|
|
buf.append(elementString);
|
|
}
|
|
if (children == true)
|
|
{
|
|
// append syntax to get all children of the path
|
|
buf.append("//*");
|
|
}
|
|
else
|
|
{
|
|
// append syntax to just represent the path, not the children
|
|
buf.append("/*");
|
|
}
|
|
|
|
return buf.toString();
|
|
}
|
|
|
|
/**
|
|
* @return Returns the categories to use for the search
|
|
*/
|
|
public String[] getCategories()
|
|
{
|
|
return this.categories;
|
|
}
|
|
|
|
/**
|
|
* @param categories The categories to set as a list of search XPATHs
|
|
*/
|
|
public void setCategories(String[] categories)
|
|
{
|
|
if (categories != null)
|
|
{
|
|
this.categories = categories;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Returns the node XPath to search in or null for all.
|
|
*/
|
|
public String getLocation()
|
|
{
|
|
return this.location;
|
|
}
|
|
|
|
/**
|
|
* @param location The node XPATH to search from or null for all..
|
|
*/
|
|
public void setLocation(String location)
|
|
{
|
|
this.location = location;
|
|
}
|
|
|
|
/**
|
|
* @return Returns the mode to use during the search (see constants)
|
|
*/
|
|
public int getMode()
|
|
{
|
|
return this.mode;
|
|
}
|
|
|
|
/**
|
|
* @param mode The mode to use during the search (see constants)
|
|
*/
|
|
public void setMode(int mode)
|
|
{
|
|
this.mode = mode;
|
|
}
|
|
|
|
/**
|
|
* @return Returns the search text string.
|
|
*/
|
|
public String getText()
|
|
{
|
|
return this.text;
|
|
}
|
|
|
|
/**
|
|
* @param text The search text string.
|
|
*/
|
|
public void setText(String text)
|
|
{
|
|
this.text = text;
|
|
}
|
|
|
|
/**
|
|
* @return Returns the contentType.
|
|
*/
|
|
public String getContentType()
|
|
{
|
|
return this.contentType;
|
|
}
|
|
|
|
/**
|
|
* @param contentType The content type to restrict attribute search against.
|
|
*/
|
|
public void setContentType(String contentType)
|
|
{
|
|
this.contentType = contentType;
|
|
}
|
|
|
|
/**
|
|
* @return Returns the folderType.
|
|
*/
|
|
public String getFolderType()
|
|
{
|
|
return this.folderType;
|
|
}
|
|
|
|
/**
|
|
* @param folderType The folder type to restrict attribute search against.
|
|
*/
|
|
public void setFolderType(String folderType)
|
|
{
|
|
this.folderType = folderType;
|
|
}
|
|
|
|
/**
|
|
* @return Returns the mimeType.
|
|
*/
|
|
public String getMimeType()
|
|
{
|
|
return this.mimeType;
|
|
}
|
|
/**
|
|
* @param mimeType The mimeType to set.
|
|
*/
|
|
public void setMimeType(String mimeType)
|
|
{
|
|
this.mimeType = mimeType;
|
|
}
|
|
|
|
/**
|
|
* Add an additional attribute to search against for simple searches
|
|
*
|
|
* @param qname QName of the attribute to search against
|
|
* @param value Value of the attribute to use
|
|
*/
|
|
public void addSimpleAttributeQuery(QName qname)
|
|
{
|
|
this.simpleSearchAdditionalAttrs.add(qname);
|
|
}
|
|
|
|
/**
|
|
* Sets the additional attribute to search against for simple searches.
|
|
*
|
|
* @param attrs The list of attributes to search against
|
|
*/
|
|
public void setSimpleSearchAdditionalAttributes(List<QName> attrs)
|
|
{
|
|
if (attrs != null)
|
|
{
|
|
this.simpleSearchAdditionalAttrs = attrs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add an additional attribute to search against
|
|
*
|
|
* @param qname QName of the attribute to search against
|
|
* @param value Value of the attribute to use
|
|
*/
|
|
public void addAttributeQuery(QName qname, String value)
|
|
{
|
|
this.queryAttributes.put(qname, value);
|
|
}
|
|
|
|
public String getAttributeQuery(QName qname)
|
|
{
|
|
return this.queryAttributes.get(qname);
|
|
}
|
|
|
|
/**
|
|
* Add an additional range attribute to search against
|
|
*
|
|
* @param qname QName of the attribute to search against
|
|
* @param lower Lower value for range
|
|
* @param upper Upper value for range
|
|
* @param inclusive True for inclusive within the range, false otherwise
|
|
*/
|
|
public void addRangeQuery(QName qname, String lower, String upper, boolean inclusive)
|
|
{
|
|
this.rangeAttributes.put(qname, new RangeProperties(qname, lower, upper, inclusive));
|
|
}
|
|
|
|
public RangeProperties getRangeProperty(QName qname)
|
|
{
|
|
return this.rangeAttributes.get(qname);
|
|
}
|
|
|
|
/**
|
|
* Add an additional fixed value attribute to search against
|
|
*
|
|
* @param qname QName of the attribute to search against
|
|
* @param value Fixed value of the attribute to use
|
|
*/
|
|
public void addFixedValueQuery(QName qname, String value)
|
|
{
|
|
this.queryFixedValues.put(qname, value);
|
|
}
|
|
|
|
public String getFixedValueQuery(QName qname)
|
|
{
|
|
return this.queryFixedValues.get(qname);
|
|
}
|
|
|
|
/**
|
|
* @return Returns if AND is forced between text terms. False (OR terms) is the default.
|
|
*/
|
|
public boolean getForceAndTerms()
|
|
{
|
|
return this.forceAndTerms;
|
|
}
|
|
|
|
/**
|
|
* @param forceAndTerms Set true to force AND between text terms. Otherwise OR is the default.
|
|
*/
|
|
public void setForceAndTerms(boolean forceAndTerms)
|
|
{
|
|
this.forceAndTerms = forceAndTerms;
|
|
}
|
|
|
|
/**
|
|
* @return this SearchContext as XML
|
|
*
|
|
* Example:
|
|
* <code>
|
|
* <?xml version="1.0" encoding="UTF-8"?>
|
|
* <search>
|
|
* <text>CDATA</text>
|
|
* <mode>int</mode>
|
|
* <location>XPath</location>
|
|
* <categories>
|
|
* <category>XPath</category>
|
|
* </categories>
|
|
* <content-type>String</content-type>
|
|
* <folder-type>String</folder-type>
|
|
* <mimetype>String</mimetype>
|
|
* <attributes>
|
|
* <attribute name="String">String</attribute>
|
|
* </attributes>
|
|
* <ranges>
|
|
* <range name="String">
|
|
* <lower>String</lower>
|
|
* <upper>String</upper>
|
|
* <inclusive>boolean</inclusive>
|
|
* </range>
|
|
* </ranges>
|
|
* <fixed-values>
|
|
* <value name="String">String</value>
|
|
* </fixed-values>
|
|
* <query>CDATA</query>
|
|
* </search>
|
|
* </code>
|
|
*/
|
|
public String toXML()
|
|
{
|
|
try
|
|
{
|
|
NamespaceService ns = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getNamespaceService();
|
|
|
|
Document doc = DocumentHelper.createDocument();
|
|
|
|
Element root = doc.addElement(ELEMENT_SEARCH);
|
|
|
|
root.addElement(ELEMENT_TEXT).addCDATA(this.text);
|
|
root.addElement(ELEMENT_MODE).addText(Integer.toString(this.mode));
|
|
if (this.location != null)
|
|
{
|
|
root.addElement(ELEMENT_LOCATION).addText(this.location);
|
|
}
|
|
|
|
Element categories = root.addElement(ELEMENT_CATEGORIES);
|
|
for (String path : this.categories)
|
|
{
|
|
categories.addElement(ELEMENT_CATEGORY).addText(path);
|
|
}
|
|
|
|
if (this.contentType != null)
|
|
{
|
|
root.addElement(ELEMENT_CONTENT_TYPE).addText(this.contentType);
|
|
}
|
|
if (this.folderType != null)
|
|
{
|
|
root.addElement(ELEMENT_FOLDER_TYPE).addText(this.folderType);
|
|
}
|
|
if (this.mimeType != null && this.mimeType.length() != 0)
|
|
{
|
|
root.addElement(ELEMENT_MIMETYPE).addText(this.mimeType);
|
|
}
|
|
|
|
Element attributes = root.addElement(ELEMENT_ATTRIBUTES);
|
|
for (QName attrName : this.queryAttributes.keySet())
|
|
{
|
|
attributes.addElement(ELEMENT_ATTRIBUTE)
|
|
.addAttribute(ELEMENT_NAME, attrName.toPrefixString(ns))
|
|
.addCDATA(this.queryAttributes.get(attrName));
|
|
}
|
|
|
|
Element ranges = root.addElement(ELEMENT_RANGES);
|
|
for (QName rangeName : this.rangeAttributes.keySet())
|
|
{
|
|
RangeProperties rangeProps = this.rangeAttributes.get(rangeName);
|
|
Element range = ranges.addElement(ELEMENT_RANGE);
|
|
range.addAttribute(ELEMENT_NAME, rangeName.toPrefixString(ns));
|
|
range.addElement(ELEMENT_LOWER).addText(rangeProps.lower);
|
|
range.addElement(ELEMENT_UPPER).addText(rangeProps.upper);
|
|
range.addElement(ELEMENT_INCLUSIVE).addText(Boolean.toString(rangeProps.inclusive));
|
|
}
|
|
|
|
Element values = root.addElement(ELEMENT_FIXED_VALUES);
|
|
for (QName valueName : this.queryFixedValues.keySet())
|
|
{
|
|
values.addElement(ELEMENT_VALUE)
|
|
.addAttribute(ELEMENT_NAME, valueName.toPrefixString(ns))
|
|
.addCDATA(this.queryFixedValues.get(valueName));
|
|
}
|
|
|
|
// outputing the full lucene query may be useful for some situations
|
|
Element query = root.addElement(ELEMENT_QUERY);
|
|
String queryString = buildQuery(0);
|
|
if (queryString != null)
|
|
{
|
|
query.addCDATA(queryString);
|
|
}
|
|
|
|
StringWriter out = new StringWriter(1024);
|
|
XMLWriter writer = new XMLWriter(OutputFormat.createPrettyPrint());
|
|
writer.setWriter(out);
|
|
writer.write(doc);
|
|
|
|
return out.toString();
|
|
}
|
|
catch (Throwable err)
|
|
{
|
|
throw new AlfrescoRuntimeException("Failed to export SearchContext to XML.", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore a SearchContext from an XML definition
|
|
*
|
|
* @param xml XML format SearchContext @see #toXML()
|
|
*/
|
|
public SearchContext fromXML(String xml)
|
|
{
|
|
try
|
|
{
|
|
NamespaceService ns = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getNamespaceService();
|
|
|
|
// get the root element
|
|
SAXReader reader = new SAXReader();
|
|
Document document = reader.read(new StringReader(xml));
|
|
Element rootElement = document.getRootElement();
|
|
Element textElement = rootElement.element(ELEMENT_TEXT);
|
|
if (textElement != null)
|
|
{
|
|
this.text = textElement.getText();
|
|
}
|
|
Element modeElement = rootElement.element(ELEMENT_MODE);
|
|
if (modeElement != null)
|
|
{
|
|
this.mode = Integer.parseInt(modeElement.getText());
|
|
}
|
|
Element locationElement = rootElement.element(ELEMENT_LOCATION);
|
|
if (locationElement != null)
|
|
{
|
|
this.location = locationElement.getText();
|
|
}
|
|
Element categoriesElement = rootElement.element(ELEMENT_CATEGORIES);
|
|
if (categoriesElement != null)
|
|
{
|
|
List<String> categories = new ArrayList<String>(4);
|
|
for (Iterator i=categoriesElement.elementIterator(ELEMENT_CATEGORY); i.hasNext(); /**/)
|
|
{
|
|
Element categoryElement = (Element)i.next();
|
|
categories.add(categoryElement.getText());
|
|
}
|
|
this.categories = categories.toArray(this.categories);
|
|
}
|
|
Element contentTypeElement = rootElement.element(ELEMENT_CONTENT_TYPE);
|
|
if (contentTypeElement != null)
|
|
{
|
|
this.contentType = contentTypeElement.getText();
|
|
}
|
|
Element folderTypeElement = rootElement.element(ELEMENT_FOLDER_TYPE);
|
|
if (folderTypeElement != null)
|
|
{
|
|
this.folderType = folderTypeElement.getText();
|
|
}
|
|
Element mimetypeElement = rootElement.element(ELEMENT_MIMETYPE);
|
|
if (mimetypeElement != null)
|
|
{
|
|
this.mimeType = mimetypeElement.getText();
|
|
}
|
|
Element attributesElement = rootElement.element(ELEMENT_ATTRIBUTES);
|
|
if (attributesElement != null)
|
|
{
|
|
for (Iterator i=attributesElement.elementIterator(ELEMENT_ATTRIBUTE); i.hasNext(); /**/)
|
|
{
|
|
Element attrElement = (Element)i.next();
|
|
QName qname = QName.createQName(attrElement.attributeValue(ELEMENT_NAME), ns);
|
|
addAttributeQuery(qname, attrElement.getText());
|
|
}
|
|
}
|
|
Element rangesElement = rootElement.element(ELEMENT_RANGES);
|
|
if (rangesElement != null)
|
|
{
|
|
for (Iterator i=rangesElement.elementIterator(ELEMENT_RANGE); i.hasNext(); /**/)
|
|
{
|
|
Element rangeElement = (Element)i.next();
|
|
Element lowerElement = rangeElement.element(ELEMENT_LOWER);
|
|
Element upperElement = rangeElement.element(ELEMENT_UPPER);
|
|
Element incElement = rangeElement.element(ELEMENT_INCLUSIVE);
|
|
if (lowerElement != null && upperElement != null && incElement != null)
|
|
{
|
|
QName qname = QName.createQName(rangeElement.attributeValue(ELEMENT_NAME), ns);
|
|
addRangeQuery(qname,
|
|
lowerElement.getText(), upperElement.getText(),
|
|
Boolean.parseBoolean(incElement.getText()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Element valuesElement = rootElement.element(ELEMENT_FIXED_VALUES);
|
|
if (valuesElement != null)
|
|
{
|
|
for (Iterator i=valuesElement.elementIterator(ELEMENT_VALUE); i.hasNext(); /**/)
|
|
{
|
|
Element valueElement = (Element)i.next();
|
|
QName qname = QName.createQName(valueElement.attributeValue(ELEMENT_NAME), ns);
|
|
addFixedValueQuery(qname, valueElement.getText());
|
|
}
|
|
}
|
|
}
|
|
catch (Throwable err)
|
|
{
|
|
throw new AlfrescoRuntimeException("Failed to import SearchContext from XML.", err);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Simple wrapper class for range query attribute properties
|
|
*/
|
|
static class RangeProperties implements Serializable
|
|
{
|
|
private static final long serialVersionUID = 5627339191207625169L;
|
|
|
|
QName qname;
|
|
String lower;
|
|
String upper;
|
|
boolean inclusive;
|
|
|
|
RangeProperties(QName qname, String lower, String upper, boolean inclusive)
|
|
{
|
|
this.qname = qname;
|
|
this.lower = lower;
|
|
this.upper = upper;
|
|
this.inclusive = inclusive;
|
|
}
|
|
}
|
|
}
|