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.api.DocumentModel;
\r
36 import org.nuxeo.ecm.core.api.DocumentModelList;
\r
37 import org.nuxeo.ecm.core.api.repository.RepositoryInstance;
\r
38 //import org.nuxeo.ecm.core.client.NuxeoClient;
\r
40 import org.collectionspace.services.jaxb.InvocableJAXBSchema;
\r
41 //import org.collectionspace.services.nuxeo.client.java.NuxeoConnector;
\r
42 //import org.collectionspace.services.nuxeo.client.java.NxConnect;
\r
43 import org.collectionspace.services.nuxeo.client.java.NuxeoClientEmbedded;
\r
44 import org.collectionspace.services.nuxeo.client.java.NuxeoConnectorEmbedded;
\r
46 import org.collectionspace.services.client.IQueryManager;
\r
47 import org.collectionspace.services.common.invocable.InvocableUtils;
\r
48 import org.collectionspace.services.common.storage.DatabaseProductType;
\r
49 import org.collectionspace.services.common.storage.JDBCTools;
\r
51 public class QueryManagerNuxeoImpl implements IQueryManager {
\r
53 private static String ECM_FULLTEXT_LIKE = "ecm:fulltext"
\r
54 + SEARCH_TERM_SEPARATOR + IQueryManager.SEARCH_LIKE;
\r
55 private static String SEARCH_LIKE_FORM = null;
\r
57 private final Logger logger = LoggerFactory
\r
58 .getLogger(QueryManagerNuxeoImpl.class);
\r
60 // Consider that letters, letter-markers, numbers, '_' and apostrophe are
\r
62 private static Pattern nonWordChars = Pattern
\r
63 .compile("[^\\p{L}\\p{M}\\p{N}_']");
\r
64 private static Pattern kwdTokenizer = Pattern.compile("(?:(['\"])(.*?)(?<!\\\\)(?>\\\\\\\\)*\\1|([^ ]+))");
\r
65 private static Pattern unescapedDblQuotes = Pattern.compile("(?<!\\\\)\"");
\r
66 private static Pattern unescapedSingleQuote = Pattern.compile("(?<!\\\\)'");
\r
67 //private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%]");
\r
68 // HACK to work around Nuxeo regression that tokenizes on '.'.
\r
69 private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%\\.]");
\r
70 private static Pattern kwdSearchHyphen = Pattern.compile(" - ");
\r
71 private static Pattern advSearchSqlWildcard = Pattern.compile(".*?[I]*LIKE\\s*\\\"\\%\\\".*?");
\r
74 private static String getLikeForm(String dataSourceName, String repositoryName) {
\r
75 if (SEARCH_LIKE_FORM == null) {
\r
77 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName);
\r
78 if (type == DatabaseProductType.MYSQL) {
\r
79 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
\r
80 } else if (type == DatabaseProductType.POSTGRESQL) {
\r
81 SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
\r
83 } catch (Exception e) {
\r
84 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
\r
87 return SEARCH_LIKE_FORM;
\r
91 public String getDatasourceName() {
\r
92 return JDBCTools.NUXEO_DATASOURCE_NAME;
\r
95 // TODO: This is currently just an example fixed query. This should
\r
97 // removed or replaced with a more generic method.
\r
102 * org.collectionspace.services.common.query.IQueryManager#execQuery(java
\r
107 public void execQuery(String queryString) {
\r
108 NuxeoClientEmbedded client = null;
\r
110 client = NuxeoConnectorEmbedded.getInstance().getClient();
\r
111 RepositoryInstance repoSession = client.openRepository();
\r
113 DocumentModelList docModelList = repoSession
\r
114 .query("SELECT * FROM Relation WHERE relations_common:subjectCsid='updated-Subject-1'");
\r
115 // DocumentModelList docModelList =
\r
116 // repoSession.query("SELECT * FROM Relation");
\r
117 // DocumentModelList docModelList =
\r
118 // repoSession.query("SELECT * FROM CollectionObject WHERE collectionobject:objectNumber='objectNumber-1251305545865'");
\r
119 for (DocumentModel docModel : docModelList) {
\r
121 .println("--------------------------------------------");
\r
122 System.out.println(docModel.getPathAsString());
\r
123 System.out.println(docModel.getName());
\r
124 System.out.println(docModel.getPropertyValue("dc:title"));
\r
125 // System.out.println("subjectCsid=" +
\r
126 // docModel.getProperty("relations_common",
\r
127 // "subjectCsid").toString());
\r
130 } catch (Exception e) {
\r
131 // TODO Auto-generated catch block
\r
132 e.printStackTrace();
\r
137 public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
\r
138 String result = null;
\r
140 // Process search term. FIXME: REM - Do we need to perform any string filtering here?
\r
142 if (advancedSearch != null && !advancedSearch.isEmpty()) {
\r
143 // Filtering of advanced searches on a single '%' char, per CSPACE-5828
\r
144 Matcher regexMatcher = advSearchSqlWildcard.matcher(advancedSearch.trim());
\r
145 if (regexMatcher.matches()) {
\r
148 StringBuffer advancedSearchWhereClause = new StringBuffer(
\r
150 result = advancedSearchWhereClause.toString();
\r
159 * @see org.collectionspace.services.common.query.IQueryManager#
\r
160 * createWhereClauseFromKeywords(java.lang.String)
\r
162 // TODO handle keywords containing escaped punctuation chars, then we need
\r
164 // search by matching on the fulltext.simpletext field.
\r
165 // TODO handle keywords containing unescaped double quotes by matching the
\r
167 // against the fulltext.simpletext field.
\r
168 // Both these require using JDBC, since we cannot get to the fulltext table
\r
171 public String createWhereClauseFromKeywords(String keywords) {
\r
172 String result = null;
\r
173 StringBuffer fullTextWhereClause = new StringBuffer();
\r
174 // Split on unescaped double quotes to handle phrases
\r
175 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
\r
176 boolean addNOT = false;
\r
177 boolean newWordSet = true;
\r
178 while (regexMatcher.find()) {
\r
179 String phrase = regexMatcher.group();
\r
180 // Not needed - already trimmed by split:
\r
181 // String trimmed = phrase.trim();
\r
182 // Ignore empty strings from match, or goofy input
\r
183 if (phrase.isEmpty())
\r
185 // Note we let OR through as is
\r
186 if("AND".equalsIgnoreCase(phrase)) {
\r
187 continue; // AND is default
\r
188 } else if("NOT".equalsIgnoreCase(phrase)) {
\r
192 // Next comment block of questionable value...
\r
194 // ignore the special chars except single quote here - can't hurt
\r
195 // TODO this should become a special function that strips things the
\r
196 // fulltext will ignore, including non-word chars and too-short
\r
198 // and escaping single quotes. Can return a boolean for anything
\r
200 // which triggers the back-up search. We can think about whether
\r
202 // short words not in a quoted phrase should trigger the backup.
\r
203 String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
\r
204 // If there are non-word chars in the phrase, we need to match the
\r
205 // phrase exactly against the fulltext table for this object
\r
206 // if(nonWordChars.matcher(trimmed).matches()) {
\r
208 // Replace problem chars with spaces. Patches CSPACE-4147,
\r
210 escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
\r
211 escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
\r
212 if(escapedAndTrimmed.isEmpty()) {
\r
213 if (logger.isDebugEnabled() == true) {
\r
214 logger.debug("Phrase reduced to empty after replacements: " + phrase);
\r
219 if (fullTextWhereClause.length()==0) {
\r
220 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
\r
223 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
\r
224 newWordSet = false;
\r
226 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
\r
229 fullTextWhereClause.append("-"); // Negate the next term
\r
232 fullTextWhereClause.append(escapedAndTrimmed);
\r
234 if (logger.isTraceEnabled() == true) {
\r
235 logger.trace("Current built whereClause is: "
\r
236 + fullTextWhereClause.toString());
\r
239 if (fullTextWhereClause.length()==0) {
\r
240 if (logger.isDebugEnabled() == true) {
\r
241 logger.debug("No usable keywords specified in string:[" + keywords + "]");
\r
244 fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
\r
247 result = fullTextWhereClause.toString();
\r
248 if (logger.isDebugEnabled()) {
\r
249 logger.debug("Final built WHERE clause is: " + result);
\r
258 * @see org.collectionspace.services.common.query.IQueryManager#
\r
259 * createWhereClauseFromKeywords(java.lang.String)
\r
261 // TODO handle keywords containing escaped punctuation chars, then we need
\r
263 // search by matching on the fulltext.simpletext field.
\r
264 // TODO handle keywords containing unescaped double quotes by matching the
\r
266 // against the fulltext.simpletext field.
\r
267 // Both these require using JDBC, since we cannot get to the fulltext table
\r
270 public String createWhereClauseForPartialMatch(String dataSourceName,
\r
271 String repositoryName,
\r
273 boolean startingWildcard,
\r
274 String partialTerm) {
\r
275 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
\r
276 if (trimmed.isEmpty()) {
\r
277 throw new RuntimeException("No partialTerm specified.");
\r
279 if(trimmed.charAt(0) == '*') {
\r
280 if(trimmed.length() == 1) { // only a star is not enough
\r
281 throw new RuntimeException("No partialTerm specified.");
\r
283 trimmed = trimmed.substring(1);
\r
284 startingWildcard = true; // force a starting wildcard match
\r
286 if (field == null || field.isEmpty()) {
\r
287 throw new RuntimeException("No match field specified.");
\r
290 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
\r
291 ptClause.append(field);
\r
292 ptClause.append(getLikeForm(dataSourceName, repositoryName));
\r
293 ptClause.append(startingWildcard?"'%":"'");
\r
294 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
\r
295 ptClause.append("%'");
\r
296 return ptClause.toString();
\r
300 * Creates a filtering where clause from docType, for invocables.
\r
305 * @return the string
\r
308 public String createWhereClauseForInvocableByDocType(String schema,
\r
310 String trimmed = (docType == null) ? "" : docType.trim();
\r
311 if (trimmed.isEmpty()) {
\r
312 throw new RuntimeException("No docType specified.");
\r
314 if (schema == null || schema.isEmpty()) {
\r
315 throw new RuntimeException("No match schema specified.");
\r
317 String wClause = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES
\r
318 + " = '" + trimmed + "'";
\r
323 * Creates a filtering where clause from invocation mode, for invocables.
\r
328 * @return the string
\r
331 public String createWhereClauseForInvocableByMode(String schema, String mode) {
\r
332 String trimmed = (mode == null) ? "" : mode.trim();
\r
333 if (trimmed.isEmpty()) {
\r
334 throw new RuntimeException("No docType specified.");
\r
336 if (schema == null || schema.isEmpty()) {
\r
337 throw new RuntimeException("No match schema specified.");
\r
339 String wClause = InvocableUtils.getPropertyNameForInvocationMode(
\r
340 schema, trimmed) + " != 0";
\r
346 * @return true if there were any chars filtered, that will require a backup
\r
347 * qualifying search on the actual text.
\r
349 private boolean filterForFullText(String input) {
\r
350 boolean fFilteredChars = false;
\r
352 return fFilteredChars;
\r
356 * Creates a query to filter a qualified (string) field according to a list of string values.
\r
357 * @param qualifiedField The schema-qualified field to filter on
\r
358 * @param filterTerms the list of one or more strings to filter on
\r
359 * @param fExclude If true, will require qualifiedField NOT match the filters strings.
\r
360 * If false, will require qualifiedField does match one of the filters strings.
\r
361 * @return queryString
\r
364 public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
\r
365 // Start with the qualified termStatus field
\r
366 StringBuilder filterClause = new StringBuilder(qualifiedField);
\r
367 if (filterTerms.length == 1) {
\r
368 filterClause.append(fExclude?" <> '":" = '");
\r
369 filterClause.append(filterTerms[0]);
\r
370 filterClause.append('\'');
\r
372 filterClause.append(fExclude?" NOT IN (":" IN (");
\r
373 for(int i=0; i<filterTerms.length; i++) {
\r
375 filterClause.append(',');
\r
377 filterClause.append('\'');
\r
378 filterClause.append(filterTerms[i]);
\r
379 filterClause.append('\'');
\r
381 filterClause.append(')');
\r
383 return filterClause.toString();
\r