2 * QueryManagerNuxeoImpl.java
4 * {Purpose of This Class}
6 * {Other Notes Relating to This Class (Optional)}
9 * $LastChangedRevision: $
12 * This document is a part of the source code and related artifacts
13 * for CollectionSpace, an open source collections management system
14 * for museums and related institutions:
16 * http://www.collectionspace.org
17 * http://wiki.collectionspace.org
19 * Copyright © 2009 {Contributing Institution}
21 * Licensed under the Educational Community License (ECL), Version 2.0.
22 * You may not use this file except in compliance with this License.
24 * You may obtain a copy of the ECL 2.0 License at
25 * https://source.collectionspace.org/collection-space/LICENSE.txt
27 package org.collectionspace.services.common.query.nuxeo;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
35 //import org.nuxeo.ecm.core.client.NuxeoClient;
40 import org.collectionspace.services.jaxb.InvocableJAXBSchema;
41 //import org.collectionspace.services.nuxeo.client.java.NuxeoConnector;
42 //import org.collectionspace.services.nuxeo.client.java.NxConnect;
44 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
45 import org.collectionspace.services.client.IQueryManager;
46 import org.collectionspace.services.common.invocable.InvocableUtils;
47 import org.collectionspace.services.common.storage.DatabaseProductType;
48 import org.collectionspace.services.common.storage.JDBCTools;
50 public class QueryManagerNuxeoImpl implements IQueryManager {
52 private static String ECM_FULLTEXT_LIKE = "ecm:fulltext"
53 + SEARCH_TERM_SEPARATOR + IQueryManager.SEARCH_LIKE;
54 private static String SEARCH_LIKE_FORM = null;
56 private final Logger logger = LoggerFactory
57 .getLogger(QueryManagerNuxeoImpl.class);
59 // Consider that letters, letter-markers, numbers, '_' and apostrophe are
61 private static Pattern nonWordChars = Pattern
62 .compile("[^\\p{L}\\p{M}\\p{N}_']");
63 private static Pattern kwdTokenizer = Pattern.compile("(?:(['\"])(.*?)(?<!\\\\)(?>\\\\\\\\)*\\1|([^ ]+))");
64 private static Pattern unescapedDblQuotes = Pattern.compile("(?<!\\\\)\"");
65 private static Pattern unescapedSingleQuote = Pattern.compile("(?<!\\\\)'");
66 //private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%]");
67 // HACK to work around Nuxeo regression that tokenizes on '.'.
68 private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%\\.]");
69 private static Pattern kwdSearchHyphen = Pattern.compile(" - ");
70 private static Pattern advSearchSqlWildcard = Pattern.compile(".*?[I]*LIKE\\s*\\\"\\%\\\".*?");
71 // Base Nuxeo document type for all CollectionSpace documents/resources
72 public static String COLLECTIONSPACE_DOCUMENT_TYPE = "CollectionSpaceDocument";
73 public static final String NUXEO_DOCUMENT_TYPE = "Document";
76 private static String getLikeForm(String dataSourceName, String repositoryName, String cspaceInstanceId) {
77 if (SEARCH_LIKE_FORM == null) {
79 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName, cspaceInstanceId);
80 if (type == DatabaseProductType.MYSQL) {
81 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
82 } else if (type == DatabaseProductType.POSTGRESQL) {
83 SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
85 } catch (Exception e) {
86 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
89 return SEARCH_LIKE_FORM;
93 public String getDatasourceName() {
94 return JDBCTools.NUXEO_DATASOURCE_NAME;
97 // TODO: This is currently just an example fixed query. This should
99 // removed or replaced with a more generic method.
104 * org.collectionspace.services.common.query.IQueryManager#execQuery(java
109 public void execQuery(String queryString) {
110 // Intentionally left blank
114 public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
115 String result = null;
117 // Process search term. FIXME: REM - Do we need to perform any string filtering here?
119 if (advancedSearch != null && !advancedSearch.isEmpty()) {
120 // Filtering of advanced searches on a single '%' char, per CSPACE-5828
121 Matcher regexMatcher = advSearchSqlWildcard.matcher(advancedSearch.trim());
122 if (regexMatcher.matches()) {
125 StringBuffer advancedSearchWhereClause = new StringBuffer(
127 result = advancedSearchWhereClause.toString();
136 * @see org.collectionspace.services.common.query.IQueryManager#
137 * createWhereClauseFromKeywords(java.lang.String)
139 // TODO handle keywords containing escaped punctuation chars, then we need
141 // search by matching on the fulltext.simpletext field.
142 // TODO handle keywords containing unescaped double quotes by matching the
144 // against the fulltext.simpletext field.
145 // Both these require using JDBC, since we cannot get to the fulltext table
148 public String createWhereClauseFromKeywords(String keywords) {
149 String result = null;
150 StringBuffer fullTextWhereClause = new StringBuffer();
151 // Split on unescaped double quotes to handle phrases
152 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
153 boolean addNOT = false;
154 boolean newWordSet = true;
155 while (regexMatcher.find()) {
156 String phrase = regexMatcher.group();
157 // Not needed - already trimmed by split:
158 // String trimmed = phrase.trim();
159 // Ignore empty strings from match, or goofy input
160 if (phrase.isEmpty())
162 // Note we let OR through as is
163 if("AND".equalsIgnoreCase(phrase)) {
164 continue; // AND is default
165 } else if("NOT".equalsIgnoreCase(phrase)) {
169 // Next comment block of questionable value...
171 // ignore the special chars except single quote here - can't hurt
172 // TODO this should become a special function that strips things the
173 // fulltext will ignore, including non-word chars and too-short
175 // and escaping single quotes. Can return a boolean for anything
177 // which triggers the back-up search. We can think about whether
179 // short words not in a quoted phrase should trigger the backup.
180 String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
181 // If there are non-word chars in the phrase, we need to match the
182 // phrase exactly against the fulltext table for this object
183 // if(nonWordChars.matcher(trimmed).matches()) {
185 // Replace problem chars with spaces. Patches CSPACE-4147,
187 escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
188 escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
189 if(escapedAndTrimmed.isEmpty()) {
190 if (logger.isDebugEnabled() == true) {
191 logger.debug("Phrase reduced to empty after replacements: " + phrase);
196 if (fullTextWhereClause.length()==0) {
197 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
200 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
203 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
206 fullTextWhereClause.append("-"); // Negate the next term
209 fullTextWhereClause.append(escapedAndTrimmed);
211 if (logger.isTraceEnabled() == true) {
212 logger.trace("Current built whereClause is: "
213 + fullTextWhereClause.toString());
216 if (fullTextWhereClause.length()==0) {
217 if (logger.isDebugEnabled() == true) {
218 logger.debug("No usable keywords specified in string:[" + keywords + "]");
221 fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
224 result = fullTextWhereClause.toString();
225 if (logger.isDebugEnabled()) {
226 logger.debug("Final built WHERE clause is: " + result);
235 * @see org.collectionspace.services.common.query.IQueryManager#
236 * createWhereClauseFromKeywords(java.lang.String)
238 // TODO handle keywords containing escaped punctuation chars, then we need
240 // search by matching on the fulltext.simpletext field.
241 // TODO handle keywords containing unescaped double quotes by matching the
243 // against the fulltext.simpletext field.
244 // Both these require using JDBC, since we cannot get to the fulltext table
247 public String createWhereClauseForPartialMatch(String dataSourceName,
248 String repositoryName,
249 String cspaceInstanceId,
251 boolean startingWildcard,
252 String partialTerm) {
253 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
254 if (trimmed.isEmpty()) {
255 throw new RuntimeException("No partialTerm specified.");
257 if(trimmed.charAt(0) == '*') {
258 if(trimmed.length() == 1) { // only a star is not enough
259 throw new RuntimeException("No partialTerm specified.");
261 trimmed = trimmed.substring(1);
262 startingWildcard = true; // force a starting wildcard match
264 if (field == null || field.isEmpty()) {
265 throw new RuntimeException("No match field specified.");
268 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
269 ptClause.append(field);
270 ptClause.append(getLikeForm(dataSourceName, repositoryName, cspaceInstanceId));
271 ptClause.append(startingWildcard?"'%":"'");
272 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
273 ptClause.append("%'");
274 return ptClause.toString();
278 * Creates a filtering where clause from docType, for invocables.
286 public String createWhereClauseForInvocableByDocType(String schema,
288 String trimmed = (docType == null) ? "" : docType.trim();
289 if (trimmed.isEmpty()) {
290 throw new RuntimeException("No docType specified.");
292 if (schema == null || schema.isEmpty()) {
293 throw new RuntimeException("No match schema specified.");
295 String wClause = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES
296 + " = '" + trimmed + "'";
301 * Creates a filtering where clause from invocation mode, for invocables.
309 public String createWhereClauseForInvocableByMode(String schema, String mode) {
310 String trimmed = (mode == null) ? "" : mode.trim();
311 if (trimmed.isEmpty()) {
312 throw new RuntimeException("No docType specified.");
314 if (schema == null || schema.isEmpty()) {
315 throw new RuntimeException("No match schema specified.");
317 String wClause = InvocableUtils.getPropertyNameForInvocationMode(
318 schema, trimmed) + " != 0";
324 * @return true if there were any chars filtered, that will require a backup
325 * qualifying search on the actual text.
327 private boolean filterForFullText(String input) {
328 boolean fFilteredChars = false;
330 return fFilteredChars;
334 * Creates a query to filter a qualified (string) field according to a list of string values.
335 * @param qualifiedField The schema-qualified field to filter on
336 * @param filterTerms the list of one or more strings to filter on
337 * @param fExclude If true, will require qualifiedField NOT match the filters strings.
338 * If false, will require qualifiedField does match one of the filters strings.
339 * @return queryString
342 public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
343 // Start with the qualified termStatus field
344 StringBuilder filterClause = new StringBuilder(qualifiedField);
345 if (filterTerms.length == 1) {
346 filterClause.append(fExclude?" <> '":" = '");
347 filterClause.append(filterTerms[0]);
348 filterClause.append('\'');
350 filterClause.append(fExclude?" NOT IN (":" IN (");
351 for(int i=0; i<filterTerms.length; i++) {
353 filterClause.append(',');
355 filterClause.append('\'');
356 filterClause.append(filterTerms[i]);
357 filterClause.append('\'');
359 filterClause.append(')');
361 return filterClause.toString();
365 public String createWhereClauseFromCsid(String csid) {
366 String trimmed = (csid == null) ? "" : csid.trim();
367 if (trimmed.isEmpty()) {
368 throw new RuntimeException("No CSID specified.");
371 return NuxeoUtils.getByNameWhereClause(csid);