]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
65fa50e7cea14902761cf5e8d9439b520bcefd32
[tmp/jakarta-migration.git] /
1 /**
2  * QueryManagerNuxeoImpl.java
3  *
4  * {Purpose of This Class}
5  *
6  * {Other Notes Relating to This Class (Optional)}
7  *
8  * $LastChangedBy: $
9  * $LastChangedRevision: $
10  * $LastChangedDate: $
11  *
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:
15  *
16  * http://www.collectionspace.org
17  * http://wiki.collectionspace.org
18  *
19  * Copyright © 2009 {Contributing Institution}
20  *
21  * Licensed under the Educational Community License (ECL), Version 2.0.
22  * You may not use this file except in compliance with this License.
23  *
24  * You may obtain a copy of the ECL 2.0 License at
25  * https://source.collectionspace.org/collection-space/LICENSE.txt
26  */
27 package org.collectionspace.services.common.query.nuxeo;
28
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.List;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34
35 import org.apache.commons.lang3.StringUtils;
36
37 import org.collectionspace.services.jaxb.InvocableJAXBSchema;
38 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
39 import org.collectionspace.services.client.IQueryManager;
40 import org.collectionspace.services.common.invocable.InvocableUtils;
41 import org.collectionspace.services.common.storage.DatabaseProductType;
42 import org.collectionspace.services.common.storage.JDBCTools;
43
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 public class QueryManagerNuxeoImpl implements IQueryManager {
48
49         private static String ECM_FULLTEXT_LIKE = "ecm:fulltext"
50                         + SEARCH_TERM_SEPARATOR + IQueryManager.SEARCH_LIKE;
51         private static String SEARCH_LIKE_FORM = null;
52
53         private final Logger logger = LoggerFactory
54                         .getLogger(QueryManagerNuxeoImpl.class);
55
56         // Consider that letters, letter-markers, numbers, '_' and apostrophe are
57         // words
58         private static Pattern nonWordChars = Pattern
59                         .compile("[^\\p{L}\\p{M}\\p{N}_']");
60         private static Pattern kwdTokenizer = Pattern.compile("(?:(['\"])(.*?)(?<!\\\\)(?>\\\\\\\\)*\\1|([^ ]+))");
61         private static Pattern unescapedDblQuotes = Pattern.compile("(?<!\\\\)\"");
62         private static Pattern unescapedSingleQuote = Pattern.compile("(?<!\\\\)'");
63         //private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%]");
64         // HACK to work around Nuxeo regression that tokenizes on '.'.
65         private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%\\.]");
66         private static Pattern kwdSearchHyphen = Pattern.compile(" - ");
67         private static Pattern advSearchSqlWildcard = Pattern.compile(".*?[I]*LIKE\\s*\\\"\\%\\\".*?");
68         // Base Nuxeo document type for all CollectionSpace documents/resources
69         public static String COLLECTIONSPACE_DOCUMENT_TYPE = "CollectionSpaceDocument";
70         public static final String NUXEO_DOCUMENT_TYPE = "Document";
71
72
73         private static String getLikeForm(String dataSourceName, String repositoryName, String cspaceInstanceId) {
74                 if (SEARCH_LIKE_FORM == null) {
75                         try {
76                                 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName, cspaceInstanceId);
77                                 if (type == DatabaseProductType.MYSQL) {
78                                         SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
79                                 } else if (type == DatabaseProductType.POSTGRESQL) {
80                                         SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
81                                 }
82                         } catch (Exception e) {
83                                 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
84                         }
85                 }
86                 return SEARCH_LIKE_FORM;
87         }
88
89         @Override
90         public String getDatasourceName() {
91                 return JDBCTools.NUXEO_DATASOURCE_NAME;
92         }
93
94         // TODO: This is currently just an example fixed query. This should
95         // eventually be
96         // removed or replaced with a more generic method.
97         /*
98          * (non-Javadoc)
99          *
100          * @see
101          * org.collectionspace.services.common.query.IQueryManager#execQuery(java
102          * .lang.String)
103          */
104         @Override
105         @Deprecated
106         public void execQuery(String queryString) {
107                 // Intentionally left blank
108         }
109
110         @Override
111         public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
112                 String result = null;
113                 //
114                 // Process search term.  FIXME: REM - Do we need to perform any string filtering here?
115                 //
116                 if (advancedSearch != null && !advancedSearch.isEmpty()) {
117                         // Filtering of advanced searches on a single '%' char, per CSPACE-5828
118                         Matcher regexMatcher = advSearchSqlWildcard.matcher(advancedSearch.trim());
119                         if (regexMatcher.matches()) {
120                             return "";
121                         }
122                         StringBuffer advancedSearchWhereClause = new StringBuffer(
123                                         advancedSearch);
124                         result = advancedSearchWhereClause.toString();
125                 }
126
127                 return result;
128         }
129
130         /*
131          * (non-Javadoc)
132          *
133          * @see org.collectionspace.services.common.query.IQueryManager#
134          * createWhereClauseFromKeywords(java.lang.String)
135          */
136         // TODO handle keywords containing escaped punctuation chars, then we need
137         // to qualify the
138         // search by matching on the fulltext.simpletext field.
139         // TODO handle keywords containing unescaped double quotes by matching the
140         // phrase
141         // against the fulltext.simpletext field.
142         // Both these require using JDBC, since we cannot get to the fulltext table
143         // in NXQL
144         @Override
145         public String createWhereClauseFromKeywords(String keywords) {
146                 String result = null;
147                 StringBuffer fullTextWhereClause = new StringBuffer();
148                 // Split on unescaped double quotes to handle phrases
149                 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
150                 boolean addNOT = false;
151                 boolean newWordSet = true;
152                 while (regexMatcher.find()) {
153                         String phrase = regexMatcher.group();
154                         // Not needed - already trimmed by split:
155                         // String trimmed = phrase.trim();
156                         // Ignore empty strings from match, or goofy input
157                         if (phrase.isEmpty())
158                                 continue;
159                         // Note we let OR through as is
160                         if("AND".equalsIgnoreCase(phrase)) {
161                                 continue;       // AND is default
162                         } else if("NOT".equalsIgnoreCase(phrase)) {
163                                 addNOT = true;
164                                 continue;
165                         }
166                         // Next comment block of questionable value...
167
168                         // ignore the special chars except single quote here - can't hurt
169                         // TODO this should become a special function that strips things the
170                         // fulltext will ignore, including non-word chars and too-short
171                         // words,
172                         // and escaping single quotes. Can return a boolean for anything
173                         // stripped,
174                         // which triggers the back-up search. We can think about whether
175                         // stripping
176                         // short words not in a quoted phrase should trigger the backup.
177                         String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
178                         // If there are non-word chars in the phrase, we need to match the
179                         // phrase exactly against the fulltext table for this object
180                         // if(nonWordChars.matcher(trimmed).matches()) {
181                         // }
182                         // Replace problem chars with spaces. Patches CSPACE-4147,
183                         // CSPACE-4106
184                         escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
185                         escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
186                         if(escapedAndTrimmed.isEmpty()) {
187                                 if (logger.isDebugEnabled() == true) {
188                                         logger.debug("Phrase reduced to empty after replacements: " + phrase);
189                                 }
190                                 continue;
191                         }
192
193                         if (fullTextWhereClause.length()==0) {
194                                 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
195                         }
196                         if (newWordSet) {
197                                 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
198                                 newWordSet = false;
199                         } else {
200                                 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
201                         }
202                         if(addNOT) {
203                                 fullTextWhereClause.append("-");        // Negate the next term
204                                 addNOT = false;
205                         }
206                         fullTextWhereClause.append(escapedAndTrimmed);
207
208                         if (logger.isTraceEnabled() == true) {
209                                 logger.trace("Current built whereClause is: "
210                                                 + fullTextWhereClause.toString());
211                         }
212                 }
213                 if (fullTextWhereClause.length()==0) {
214                         if (logger.isDebugEnabled() == true) {
215                                 logger.debug("No usable keywords specified in string:[" + keywords + "]");
216                         }
217                 } else {
218                         fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
219                 }
220
221                 result = fullTextWhereClause.toString();
222                 if (logger.isDebugEnabled()) {
223                         logger.debug("Final built WHERE clause is: " + result);
224                 }
225
226                 return result;
227         }
228
229         /*
230          * (non-Javadoc)
231          *
232          * @see org.collectionspace.services.common.query.IQueryManager#
233          * createWhereClauseFromKeywords(java.lang.String)
234          */
235         // TODO handle keywords containing escaped punctuation chars, then we need
236         // to qualify the
237         // search by matching on the fulltext.simpletext field.
238         // TODO handle keywords containing unescaped double quotes by matching the
239         // phrase
240         // against the fulltext.simpletext field.
241         // Both these require using JDBC, since we cannot get to the fulltext table
242         // in NXQL
243         @Override
244         public String createWhereClauseForPartialMatch(String dataSourceName,
245                         String repositoryName,
246                         String cspaceInstanceId,
247                         String field,
248                         boolean startingWildcard,
249                         String partialTerm) {
250                 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
251                 if (trimmed.isEmpty()) {
252                         throw new RuntimeException("No partialTerm specified.");
253                 }
254                 if(trimmed.charAt(0) == '*') {
255                         if(trimmed.length() == 1) { // only a star is not enough
256                                 throw new RuntimeException("No partialTerm specified.");
257                         }
258                         trimmed = trimmed.substring(1);
259                         startingWildcard = true;                // force a starting wildcard match
260                 }
261                 if (field == null || field.isEmpty()) {
262                         throw new RuntimeException("No match field specified.");
263                 }
264
265                 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
266                 ptClause.append(field);
267                 ptClause.append(getLikeForm(dataSourceName, repositoryName, cspaceInstanceId));
268                 ptClause.append(startingWildcard?"'%":"'");
269                 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
270                 ptClause.append("%'");
271                 return ptClause.toString();
272         }
273
274         /**
275          * Creates a filtering where clause from docType, for invocables.
276          *
277          * @param docType
278          *            the docType
279          *
280          * @return the string
281          */
282         @Override
283         public String createWhereClauseForInvocableByDocType(String schema,
284                         String docType) {
285                 String trimmed = (docType == null) ? "" : docType.trim();
286                 if (trimmed.isEmpty()) {
287                         throw new RuntimeException("No docType specified.");
288                 }
289                 if (schema == null || schema.isEmpty()) {
290                         throw new RuntimeException("No match schema specified.");
291                 }
292                 String wClause = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES
293                                 + " = '" + trimmed + "'";
294                 return wClause;
295         }
296
297         /**
298          * Creates a filtering where clause from invocation mode, for invocables.
299          *
300          * @param mode
301          *            the mode
302          *
303          * @return the string
304          */
305         @Override
306         public String createWhereClauseForInvocableByMode(String schema, String mode) {
307                 return createWhereClauseForInvocableByMode(schema, Arrays.asList(mode));
308         }
309
310         @Override
311         public String createWhereClauseForInvocableByMode(String schema, List<String> modes) {
312                 if (schema == null || schema.isEmpty()) {
313                         throw new RuntimeException("No match schema specified.");
314                 }
315
316                 if (modes == null || modes.isEmpty()) {
317                         throw new RuntimeException("No mode specified.");
318                 }
319
320                 List<String> whereClauses = new ArrayList<String>();
321
322                 for (String mode : modes) {
323                         String propName = InvocableUtils.getPropertyNameForInvocationMode(schema, mode.trim());
324
325                         if (propName != null && !propName.isEmpty()) {
326                                 whereClauses.add(propName + " != 0");
327                         }
328                 }
329
330                 if (whereClauses.size() > 1) {
331                         return ("(" + StringUtils.join(whereClauses, " OR ") + ")");
332                 }
333
334                 if (whereClauses.size() > 0) {
335                         return whereClauses.get(0);
336                 }
337
338                 return "";
339         }
340
341         /**
342          * @param input
343          * @return true if there were any chars filtered, that will require a backup
344          *         qualifying search on the actual text.
345          */
346         private boolean filterForFullText(String input) {
347                 boolean fFilteredChars = false;
348
349                 return fFilteredChars;
350         }
351
352         /**
353          * Creates a query to filter a qualified (string) field according to a list of string values.
354          * @param qualifiedField The schema-qualified field to filter on
355          * @param filterTerms the list of one or more strings to filter on
356          * @param fExclude If true, will require qualifiedField NOT match the filters strings.
357          *                                      If false, will require qualifiedField does match one of the filters strings.
358          * @return queryString
359          */
360         @Override
361         public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
362         // Start with the qualified termStatus field
363         StringBuilder filterClause = new StringBuilder(qualifiedField);
364         if (filterTerms.length == 1) {
365                 filterClause.append(fExclude?" <> '":" = '");
366                 filterClause.append(filterTerms[0]);
367                 filterClause.append('\'');
368         } else {
369                 filterClause.append(fExclude?" NOT IN (":" IN (");
370                 for(int i=0; i<filterTerms.length; i++) {
371                         if(i>0) {
372                                 filterClause.append(',');
373                         }
374                         filterClause.append('\'');
375                         filterClause.append(filterTerms[i]);
376                         filterClause.append('\'');
377                 }
378                 filterClause.append(')');
379         }
380         return filterClause.toString();
381         }
382
383         @Override
384         public String createWhereClauseFromCsid(String csid) {
385                 String trimmed = (csid == null) ? "" : csid.trim();
386                 if (trimmed.isEmpty()) {
387                         throw new RuntimeException("No CSID specified.");
388                 }
389
390                 return NuxeoUtils.getByNameWhereClause(csid);
391         }
392
393 }