2 * QueryManagerNuxeoImpl.java
\r
4 * {Purpose of This Class}
\r
6 * {Other Notes Relating to This Class (Optional)}
\r
9 * $LastChangedRevision: $
\r
10 * $LastChangedDate: $
\r
12 * This document is a part of the source code and related artifacts
\r
13 * for CollectionSpace, an open source collections management system
\r
14 * for museums and related institutions:
\r
16 * http://www.collectionspace.org
\r
17 * http://wiki.collectionspace.org
\r
19 * Copyright © 2009 {Contributing Institution}
\r
21 * Licensed under the Educational Community License (ECL), Version 2.0.
\r
22 * You may not use this file except in compliance with this License.
\r
24 * You may obtain a copy of the ECL 2.0 License at
\r
25 * https://source.collectionspace.org/collection-space/LICENSE.txt
\r
27 package org.collectionspace.services.common.query.nuxeo;
\r
29 import org.slf4j.Logger;
\r
30 import org.slf4j.LoggerFactory;
\r
32 import java.util.regex.Matcher;
\r
33 import java.util.regex.Pattern;
\r
35 //import org.nuxeo.ecm.core.client.NuxeoClient;
\r
37 import org.collectionspace.services.jaxb.InvocableJAXBSchema;
\r
38 //import org.collectionspace.services.nuxeo.client.java.NuxeoConnector;
\r
39 //import org.collectionspace.services.nuxeo.client.java.NxConnect;
\r
41 import org.collectionspace.services.client.IQueryManager;
\r
42 import org.collectionspace.services.common.invocable.InvocableUtils;
\r
43 import org.collectionspace.services.common.storage.DatabaseProductType;
\r
44 import org.collectionspace.services.common.storage.JDBCTools;
\r
46 public class QueryManagerNuxeoImpl implements IQueryManager {
\r
48 private static String ECM_FULLTEXT_LIKE = "ecm:fulltext"
\r
49 + SEARCH_TERM_SEPARATOR + IQueryManager.SEARCH_LIKE;
\r
50 private static String SEARCH_LIKE_FORM = null;
\r
52 private final Logger logger = LoggerFactory
\r
53 .getLogger(QueryManagerNuxeoImpl.class);
\r
55 // Consider that letters, letter-markers, numbers, '_' and apostrophe are
\r
57 private static Pattern nonWordChars = Pattern
\r
58 .compile("[^\\p{L}\\p{M}\\p{N}_']");
\r
59 private static Pattern kwdTokenizer = Pattern.compile("(?:(['\"])(.*?)(?<!\\\\)(?>\\\\\\\\)*\\1|([^ ]+))");
\r
60 private static Pattern unescapedDblQuotes = Pattern.compile("(?<!\\\\)\"");
\r
61 private static Pattern unescapedSingleQuote = Pattern.compile("(?<!\\\\)'");
\r
62 //private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%]");
\r
63 // HACK to work around Nuxeo regression that tokenizes on '.'.
\r
64 private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%\\.]");
\r
65 private static Pattern kwdSearchHyphen = Pattern.compile(" - ");
\r
66 private static Pattern advSearchSqlWildcard = Pattern.compile(".*?[I]*LIKE\\s*\\\"\\%\\\".*?");
\r
69 private static String getLikeForm(String dataSourceName, String repositoryName, String cspaceInstanceId) {
\r
70 if (SEARCH_LIKE_FORM == null) {
\r
72 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName, cspaceInstanceId);
\r
73 if (type == DatabaseProductType.MYSQL) {
\r
74 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
\r
75 } else if (type == DatabaseProductType.POSTGRESQL) {
\r
76 SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
\r
78 } catch (Exception e) {
\r
79 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
\r
82 return SEARCH_LIKE_FORM;
\r
86 public String getDatasourceName() {
\r
87 return JDBCTools.NUXEO_DATASOURCE_NAME;
\r
90 // TODO: This is currently just an example fixed query. This should
\r
92 // removed or replaced with a more generic method.
\r
97 * org.collectionspace.services.common.query.IQueryManager#execQuery(java
\r
102 public void execQuery(String queryString) {
\r
103 // Intentionally left blank
\r
107 public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
\r
108 String result = null;
\r
110 // Process search term. FIXME: REM - Do we need to perform any string filtering here?
\r
112 if (advancedSearch != null && !advancedSearch.isEmpty()) {
\r
113 // Filtering of advanced searches on a single '%' char, per CSPACE-5828
\r
114 Matcher regexMatcher = advSearchSqlWildcard.matcher(advancedSearch.trim());
\r
115 if (regexMatcher.matches()) {
\r
118 StringBuffer advancedSearchWhereClause = new StringBuffer(
\r
120 result = advancedSearchWhereClause.toString();
\r
129 * @see org.collectionspace.services.common.query.IQueryManager#
\r
130 * createWhereClauseFromKeywords(java.lang.String)
\r
132 // TODO handle keywords containing escaped punctuation chars, then we need
\r
134 // search by matching on the fulltext.simpletext field.
\r
135 // TODO handle keywords containing unescaped double quotes by matching the
\r
137 // against the fulltext.simpletext field.
\r
138 // Both these require using JDBC, since we cannot get to the fulltext table
\r
141 public String createWhereClauseFromKeywords(String keywords) {
\r
142 String result = null;
\r
143 StringBuffer fullTextWhereClause = new StringBuffer();
\r
144 // Split on unescaped double quotes to handle phrases
\r
145 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
\r
146 boolean addNOT = false;
\r
147 boolean newWordSet = true;
\r
148 while (regexMatcher.find()) {
\r
149 String phrase = regexMatcher.group();
\r
150 // Not needed - already trimmed by split:
\r
151 // String trimmed = phrase.trim();
\r
152 // Ignore empty strings from match, or goofy input
\r
153 if (phrase.isEmpty())
\r
155 // Note we let OR through as is
\r
156 if("AND".equalsIgnoreCase(phrase)) {
\r
157 continue; // AND is default
\r
158 } else if("NOT".equalsIgnoreCase(phrase)) {
\r
162 // Next comment block of questionable value...
\r
164 // ignore the special chars except single quote here - can't hurt
\r
165 // TODO this should become a special function that strips things the
\r
166 // fulltext will ignore, including non-word chars and too-short
\r
168 // and escaping single quotes. Can return a boolean for anything
\r
170 // which triggers the back-up search. We can think about whether
\r
172 // short words not in a quoted phrase should trigger the backup.
\r
173 String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
\r
174 // If there are non-word chars in the phrase, we need to match the
\r
175 // phrase exactly against the fulltext table for this object
\r
176 // if(nonWordChars.matcher(trimmed).matches()) {
\r
178 // Replace problem chars with spaces. Patches CSPACE-4147,
\r
180 escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
\r
181 escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
\r
182 if(escapedAndTrimmed.isEmpty()) {
\r
183 if (logger.isDebugEnabled() == true) {
\r
184 logger.debug("Phrase reduced to empty after replacements: " + phrase);
\r
189 if (fullTextWhereClause.length()==0) {
\r
190 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
\r
193 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
\r
194 newWordSet = false;
\r
196 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
\r
199 fullTextWhereClause.append("-"); // Negate the next term
\r
202 fullTextWhereClause.append(escapedAndTrimmed);
\r
204 if (logger.isTraceEnabled() == true) {
\r
205 logger.trace("Current built whereClause is: "
\r
206 + fullTextWhereClause.toString());
\r
209 if (fullTextWhereClause.length()==0) {
\r
210 if (logger.isDebugEnabled() == true) {
\r
211 logger.debug("No usable keywords specified in string:[" + keywords + "]");
\r
214 fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
\r
217 result = fullTextWhereClause.toString();
\r
218 if (logger.isDebugEnabled()) {
\r
219 logger.debug("Final built WHERE clause is: " + result);
\r
228 * @see org.collectionspace.services.common.query.IQueryManager#
\r
229 * createWhereClauseFromKeywords(java.lang.String)
\r
231 // TODO handle keywords containing escaped punctuation chars, then we need
\r
233 // search by matching on the fulltext.simpletext field.
\r
234 // TODO handle keywords containing unescaped double quotes by matching the
\r
236 // against the fulltext.simpletext field.
\r
237 // Both these require using JDBC, since we cannot get to the fulltext table
\r
240 public String createWhereClauseForPartialMatch(String dataSourceName,
\r
241 String repositoryName,
\r
242 String cspaceInstanceId,
\r
244 boolean startingWildcard,
\r
245 String partialTerm) {
\r
246 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
\r
247 if (trimmed.isEmpty()) {
\r
248 throw new RuntimeException("No partialTerm specified.");
\r
250 if(trimmed.charAt(0) == '*') {
\r
251 if(trimmed.length() == 1) { // only a star is not enough
\r
252 throw new RuntimeException("No partialTerm specified.");
\r
254 trimmed = trimmed.substring(1);
\r
255 startingWildcard = true; // force a starting wildcard match
\r
257 if (field == null || field.isEmpty()) {
\r
258 throw new RuntimeException("No match field specified.");
\r
261 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
\r
262 ptClause.append(field);
\r
263 ptClause.append(getLikeForm(dataSourceName, repositoryName, cspaceInstanceId));
\r
264 ptClause.append(startingWildcard?"'%":"'");
\r
265 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
\r
266 ptClause.append("%'");
\r
267 return ptClause.toString();
\r
271 * Creates a filtering where clause from docType, for invocables.
\r
276 * @return the string
\r
279 public String createWhereClauseForInvocableByDocType(String schema,
\r
281 String trimmed = (docType == null) ? "" : docType.trim();
\r
282 if (trimmed.isEmpty()) {
\r
283 throw new RuntimeException("No docType specified.");
\r
285 if (schema == null || schema.isEmpty()) {
\r
286 throw new RuntimeException("No match schema specified.");
\r
288 String wClause = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES
\r
289 + " = '" + trimmed + "'";
\r
294 * Creates a filtering where clause from invocation mode, for invocables.
\r
299 * @return the string
\r
302 public String createWhereClauseForInvocableByMode(String schema, String mode) {
\r
303 String trimmed = (mode == null) ? "" : mode.trim();
\r
304 if (trimmed.isEmpty()) {
\r
305 throw new RuntimeException("No docType specified.");
\r
307 if (schema == null || schema.isEmpty()) {
\r
308 throw new RuntimeException("No match schema specified.");
\r
310 String wClause = InvocableUtils.getPropertyNameForInvocationMode(
\r
311 schema, trimmed) + " != 0";
\r
317 * @return true if there were any chars filtered, that will require a backup
\r
318 * qualifying search on the actual text.
\r
320 private boolean filterForFullText(String input) {
\r
321 boolean fFilteredChars = false;
\r
323 return fFilteredChars;
\r
327 * Creates a query to filter a qualified (string) field according to a list of string values.
\r
328 * @param qualifiedField The schema-qualified field to filter on
\r
329 * @param filterTerms the list of one or more strings to filter on
\r
330 * @param fExclude If true, will require qualifiedField NOT match the filters strings.
\r
331 * If false, will require qualifiedField does match one of the filters strings.
\r
332 * @return queryString
\r
335 public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
\r
336 // Start with the qualified termStatus field
\r
337 StringBuilder filterClause = new StringBuilder(qualifiedField);
\r
338 if (filterTerms.length == 1) {
\r
339 filterClause.append(fExclude?" <> '":" = '");
\r
340 filterClause.append(filterTerms[0]);
\r
341 filterClause.append('\'');
\r
343 filterClause.append(fExclude?" NOT IN (":" IN (");
\r
344 for(int i=0; i<filterTerms.length; i++) {
\r
346 filterClause.append(',');
\r
348 filterClause.append('\'');
\r
349 filterClause.append(filterTerms[i]);
\r
350 filterClause.append('\'');
\r
352 filterClause.append(')');
\r
354 return filterClause.toString();
\r