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
72 private static String getLikeForm(String dataSourceName, String repositoryName) {
\r
73 if (SEARCH_LIKE_FORM == null) {
\r
75 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName);
\r
76 if (type == DatabaseProductType.MYSQL) {
\r
77 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
\r
78 } else if (type == DatabaseProductType.POSTGRESQL) {
\r
79 SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
\r
81 } catch (Exception e) {
\r
82 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
\r
85 return SEARCH_LIKE_FORM;
\r
89 public String getDatasourceName() {
\r
90 return JDBCTools.NUXEO_DATASOURCE_NAME;
\r
93 // TODO: This is currently just an example fixed query. This should
\r
95 // removed or replaced with a more generic method.
\r
100 * org.collectionspace.services.common.query.IQueryManager#execQuery(java
\r
105 public void execQuery(String queryString) {
\r
106 NuxeoClientEmbedded client = null;
\r
108 client = NuxeoConnectorEmbedded.getInstance().getClient();
\r
109 RepositoryInstance repoSession = client.openRepository();
\r
111 DocumentModelList docModelList = repoSession
\r
112 .query("SELECT * FROM Relation WHERE relations_common:subjectCsid='updated-Subject-1'");
\r
113 // DocumentModelList docModelList =
\r
114 // repoSession.query("SELECT * FROM Relation");
\r
115 // DocumentModelList docModelList =
\r
116 // repoSession.query("SELECT * FROM CollectionObject WHERE collectionobject:objectNumber='objectNumber-1251305545865'");
\r
117 for (DocumentModel docModel : docModelList) {
\r
119 .println("--------------------------------------------");
\r
120 System.out.println(docModel.getPathAsString());
\r
121 System.out.println(docModel.getName());
\r
122 System.out.println(docModel.getPropertyValue("dc:title"));
\r
123 // System.out.println("subjectCsid=" +
\r
124 // docModel.getProperty("relations_common",
\r
125 // "subjectCsid").toString());
\r
128 } catch (Exception e) {
\r
129 // TODO Auto-generated catch block
\r
130 e.printStackTrace();
\r
135 public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
\r
136 String result = null;
\r
138 // Process search term. FIXME: REM - Do we need to perform and string filtering here?
\r
140 if (advancedSearch != null && !advancedSearch.isEmpty()) {
\r
141 StringBuffer advancedSearchWhereClause = new StringBuffer(
\r
143 result = advancedSearchWhereClause.toString();
\r
152 * @see org.collectionspace.services.common.query.IQueryManager#
\r
153 * createWhereClauseFromKeywords(java.lang.String)
\r
155 // TODO handle keywords containing escaped punctuation chars, then we need
\r
157 // search by matching on the fulltext.simpletext field.
\r
158 // TODO handle keywords containing unescaped double quotes by matching the
\r
160 // against the fulltext.simpletext field.
\r
161 // Both these require using JDBC, since we cannot get to the fulltext table
\r
164 public String createWhereClauseFromKeywords(String keywords) {
\r
165 String result = null;
\r
166 StringBuffer fullTextWhereClause = new StringBuffer();
\r
167 // Split on unescaped double quotes to handle phrases
\r
168 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
\r
169 boolean addNOT = false;
\r
170 boolean newWordSet = true;
\r
171 while (regexMatcher.find()) {
\r
172 String phrase = regexMatcher.group();
\r
173 // Not needed - already trimmed by split:
\r
174 // String trimmed = phrase.trim();
\r
175 // Ignore empty strings from match, or goofy input
\r
176 if (phrase.isEmpty())
\r
178 // Note we let OR through as is
\r
179 if("AND".equalsIgnoreCase(phrase)) {
\r
180 continue; // AND is default
\r
181 } else if("NOT".equalsIgnoreCase(phrase)) {
\r
185 // Next comment block of questionable value...
\r
187 // ignore the special chars except single quote here - can't hurt
\r
188 // TODO this should become a special function that strips things the
\r
189 // fulltext will ignore, including non-word chars and too-short
\r
191 // and escaping single quotes. Can return a boolean for anything
\r
193 // which triggers the back-up search. We can think about whether
\r
195 // short words not in a quoted phrase should trigger the backup.
\r
196 String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
\r
197 // If there are non-word chars in the phrase, we need to match the
\r
198 // phrase exactly against the fulltext table for this object
\r
199 // if(nonWordChars.matcher(trimmed).matches()) {
\r
201 // Replace problem chars with spaces. Patches CSPACE-4147,
\r
203 escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
\r
204 escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
\r
205 if(escapedAndTrimmed.isEmpty()) {
\r
206 if (logger.isDebugEnabled() == true) {
\r
207 logger.debug("Phrase reduced to empty after replacements: " + phrase);
\r
212 if (fullTextWhereClause.length()==0) {
\r
213 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
\r
216 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
\r
217 newWordSet = false;
\r
219 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
\r
222 fullTextWhereClause.append("-"); // Negate the next term
\r
225 fullTextWhereClause.append(escapedAndTrimmed);
\r
227 if (logger.isTraceEnabled() == true) {
\r
228 logger.trace("Current built whereClause is: "
\r
229 + fullTextWhereClause.toString());
\r
232 if (fullTextWhereClause.length()==0) {
\r
233 if (logger.isDebugEnabled() == true) {
\r
234 logger.debug("No usable keywords specified in string:[" + keywords + "]");
\r
237 fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
\r
240 result = fullTextWhereClause.toString();
\r
241 if (logger.isDebugEnabled()) {
\r
242 logger.debug("Final built WHERE clause is: " + result);
\r
251 * @see org.collectionspace.services.common.query.IQueryManager#
\r
252 * createWhereClauseFromKeywords(java.lang.String)
\r
254 // TODO handle keywords containing escaped punctuation chars, then we need
\r
256 // search by matching on the fulltext.simpletext field.
\r
257 // TODO handle keywords containing unescaped double quotes by matching the
\r
259 // against the fulltext.simpletext field.
\r
260 // Both these require using JDBC, since we cannot get to the fulltext table
\r
263 public String createWhereClauseForPartialMatch(String dataSourceName,
\r
264 String repositoryName,
\r
266 boolean startingWildcard,
\r
267 String partialTerm) {
\r
268 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
\r
269 if (trimmed.isEmpty()) {
\r
270 throw new RuntimeException("No partialTerm specified.");
\r
272 if(trimmed.charAt(0) == '*') {
\r
273 if(trimmed.length() == 1) { // only a star is not enough
\r
274 throw new RuntimeException("No partialTerm specified.");
\r
276 trimmed = trimmed.substring(1);
\r
277 startingWildcard = true; // force a starting wildcard match
\r
279 if (field == null || field.isEmpty()) {
\r
280 throw new RuntimeException("No match field specified.");
\r
283 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
\r
284 ptClause.append(field);
\r
285 ptClause.append(getLikeForm(dataSourceName, repositoryName));
\r
286 ptClause.append(startingWildcard?"'%":"'");
\r
287 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
\r
288 ptClause.append("%'");
\r
289 return ptClause.toString();
\r
293 * Creates a filtering where clause from docType, for invocables.
\r
298 * @return the string
\r
301 public String createWhereClauseForInvocableByDocType(String schema,
\r
303 String trimmed = (docType == null) ? "" : docType.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 = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES
\r
311 + " = '" + trimmed + "'";
\r
316 * Creates a filtering where clause from invocation mode, for invocables.
\r
321 * @return the string
\r
324 public String createWhereClauseForInvocableByMode(String schema, String mode) {
\r
325 String trimmed = (mode == null) ? "" : mode.trim();
\r
326 if (trimmed.isEmpty()) {
\r
327 throw new RuntimeException("No docType specified.");
\r
329 if (schema == null || schema.isEmpty()) {
\r
330 throw new RuntimeException("No match schema specified.");
\r
332 String wClause = InvocableUtils.getPropertyNameForInvocationMode(
\r
333 schema, trimmed) + " != 0";
\r
339 * @return true if there were any chars filtered, that will require a backup
\r
340 * qualifying search on the actual text.
\r
342 private boolean filterForFullText(String input) {
\r
343 boolean fFilteredChars = false;
\r
345 return fFilteredChars;
\r
349 * Creates a query to filter a qualified (string) field according to a list of string values.
\r
350 * @param qualifiedField The schema-qualified field to filter on
\r
351 * @param filterTerms the list of one or more strings to filter on
\r
352 * @param fExclude If true, will require qualifiedField NOT match the filters strings.
\r
353 * If false, will require qualifiedField does match one of the filters strings.
\r
354 * @return queryString
\r
357 public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
\r
358 // Start with the qualified termStatus field
\r
359 StringBuilder filterClause = new StringBuilder(qualifiedField);
\r
360 if (filterTerms.length == 1) {
\r
361 filterClause.append(fExclude?" <> '":" = '");
\r
362 filterClause.append(filterTerms[0]);
\r
363 filterClause.append('\'');
\r
365 filterClause.append(fExclude?" NOT IN (":" IN (");
\r
366 for(int i=0; i<filterTerms.length; i++) {
\r
368 filterClause.append(',');
\r
370 filterClause.append('\'');
\r
371 filterClause.append(filterTerms[i]);
\r
372 filterClause.append('\'');
\r
374 filterClause.append(')');
\r
376 return filterClause.toString();
\r