001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.storage.search.implementation.database.informix.excalibur;
011:
012: import java.io.*;
013: import java.util.*;
014:
015: import org.mmbase.bridge.Field;
016: import org.mmbase.module.core.*;
017: import org.mmbase.storage.StorageManagerFactory;
018: import org.mmbase.storage.search.*;
019: import org.mmbase.storage.search.implementation.database.*;
020: import org.mmbase.util.logging.*;
021: import org.w3c.dom.*;
022: import org.xml.sax.*;
023:
024: /**
025: * The Etx query handler adds support for Excalibur Text Search constraints,
026: * when used with an Informix database and an Excalibur Text Search datablade.
027: * This class is provided as a coding example of a ChainedSqlHandler.
028: * <p>
029: * On initialization, the handler reads a list of etx-indices from a
030: * configuration file.
031: * This configurationfile must be named <em>etxindices.xml</em> and located
032: * inside the <em>databases</em> configuration directory.
033: * It's dtd is located in the directory
034: * <code>org.mmbase.storage.search.implementation.database.informix.excalibur.resources</code>
035: * in the MMBase source tree and
036: * <a href="http://www.mmbase.org/dtd/etxindices.dtd">here</a> online.
037: *
038: * @author Rob van Maris
039: * @version $Id: EtxSqlHandler.java,v 1.10 2007/06/12 10:59:41 michiel Exp $
040: * @since MMBase-1.7
041: */
042: // TODO RvM: (later) add javadoc, elaborate on overwritten methods.
043: public class EtxSqlHandler extends ChainedSqlHandler implements
044: SqlHandler {
045:
046: private static final Logger log = Logging
047: .getLoggerInstance(EtxSqlHandler.class);
048:
049: /**
050: * The indexed fields, stored as {@link #BuilderField BuilderField}
051: * instances.
052: */
053: private Set<String> indexedFields = new HashSet<String>();
054:
055: /**
056: * Creates a new instance of EtxueryHandler.
057: *
058: * @param successor Successor in chain or responsibility.
059: */
060: public EtxSqlHandler(SqlHandler successor) throws IOException {
061: super (successor);
062: init();
063: }
064:
065: // javadoc is inherited
066: public void appendConstraintToSql(StringBuilder sb,
067: Constraint constraint, SearchQuery query, boolean inverse,
068: boolean inComposite) throws SearchQueryException {
069: // Net effect of inverse setting with constraint inverse property.
070: boolean overallInverse = inverse ^ constraint.isInverse();
071:
072: if (constraint instanceof StringSearchConstraint) {
073: // TODO: test for support, else throw exception
074: // TODO: support maxNumber for query with etx constraint.
075: StringSearchConstraint stringSearchConstraint = (StringSearchConstraint) constraint;
076: StepField field = stringSearchConstraint.getField();
077: Map<String, Object> parameters = stringSearchConstraint
078: .getParameters();
079:
080: // TODO: how to implement inverse,
081: // it is actually more complicated than this:
082: if (overallInverse) {
083: sb.append("NOT ");
084: }
085: sb.append("etx_contains(").append(
086: getAllowedValue(field.getStep().getAlias()))
087: .append(".").append(
088: getAllowedValue(field.getFieldName()))
089: .append(", Row('");
090:
091: Iterator<String> iSearchTerms = stringSearchConstraint
092: .getSearchTerms().iterator();
093: while (iSearchTerms.hasNext()) {
094: String searchTerm = iSearchTerms.next();
095: sb.append(searchTerm);
096: if (iSearchTerms.hasNext()) {
097: sb.append(" ");
098: }
099: }
100: sb.append("', '");
101: switch (stringSearchConstraint.getSearchType()) {
102: case StringSearchConstraint.SEARCH_TYPE_WORD_ORIENTED:
103: sb.append("SEARCH_TYPE = WORD");
104: break;
105:
106: case StringSearchConstraint.SEARCH_TYPE_PHRASE_ORIENTED:
107: sb.append("SEARCH_TYPE = PHRASE_EXACT");
108: break;
109:
110: case StringSearchConstraint.SEARCH_TYPE_PROXIMITY_ORIENTED:
111: Integer proximityLimit = (Integer) parameters
112: .get(StringSearchConstraint.PARAM_PROXIMITY_LIMIT);
113: if (proximityLimit == null) {
114: throw new IllegalStateException(
115: "Parameter PARAM_PROXIMITY_LIMIT not set "
116: + "while trying to perform proximity oriented search.");
117: }
118: sb.append("SEARCH_TYPE = PROX_SEARCH(").append(
119: proximityLimit).append(")");
120: break;
121:
122: default:
123: throw new IllegalStateException(
124: "Invalid searchtype value: "
125: + stringSearchConstraint
126: .getSearchType());
127: }
128:
129: switch (stringSearchConstraint.getMatchType()) {
130: case StringSearchConstraint.MATCH_TYPE_FUZZY:
131: Float fuzziness = (Float) parameters
132: .get(StringSearchConstraint.PARAM_FUZZINESS);
133: int wordScore = Math
134: .round(100 * fuzziness.floatValue());
135: sb.append(" & PATTERN_ALL & WORD_SCORE = ").append(
136: wordScore);
137: break;
138:
139: case StringSearchConstraint.MATCH_TYPE_LITERAL:
140: break;
141:
142: case StringSearchConstraint.MATCH_TYPE_SYNONYM:
143: log
144: .warn("Synonym matching not supported. Executing this query with literal matching instead: "
145: + query);
146: break;
147:
148: default:
149: throw new IllegalStateException(
150: "Invalid matchtype value: "
151: + stringSearchConstraint.getMatchType());
152: }
153:
154: sb.append("'))");
155:
156: } else {
157: getSuccessor().appendConstraintToSql(sb, constraint, query,
158: inverse, inComposite);
159: }
160: }
161:
162: // javadoc is inherited
163: public int getSupportLevel(int feature, SearchQuery query)
164: throws SearchQueryException {
165: int support;
166: switch (feature) {
167: case SearchQueryHandler.FEATURE_MAX_NUMBER:
168: // optimal with etx index on field, and constraint is
169: // StringSearchConstraint, with no additonal constraints.
170: Constraint constraint = query.getConstraint();
171: if (constraint != null
172: && constraint instanceof StringSearchConstraint
173: && hasEtxIndex(((StringSearchConstraint) constraint)
174: .getField())
175: && !hasAdditionalConstraints(query)) {
176: support = SearchQueryHandler.SUPPORT_OPTIMAL;
177: } else {
178: support = getSuccessor()
179: .getSupportLevel(feature, query);
180: }
181: break;
182: default:
183: support = getSuccessor().getSupportLevel(feature, query);
184: }
185: return support;
186: }
187:
188: // javadoc is inherited
189: public int getSupportLevel(Constraint constraint, SearchQuery query)
190: throws SearchQueryException {
191: int support;
192:
193: if (constraint instanceof StringSearchConstraint
194: && hasEtxIndex(((StringSearchConstraint) constraint)
195: .getField())) {
196: StringSearchConstraint stringSearchConstraint = (StringSearchConstraint) constraint;
197: // StringSearchConstraint on field with etx index:
198: // - none if matchtype = MATCH_TYPE_SYNONYM
199: // - otherwise: weak support if other stringsearch constraints are present
200: // - otherwise: optimal support
201: if (stringSearchConstraint.getMatchType() == StringSearchConstraint.MATCH_TYPE_SYNONYM) {
202: support = SearchQueryHandler.SUPPORT_NONE;
203: } else if (containsOtherStringSearchConstraints(query
204: .getConstraint(), stringSearchConstraint)) {
205: support = SearchQueryHandler.SUPPORT_WEAK;
206: } else {
207: support = SearchQueryHandler.SUPPORT_OPTIMAL;
208: }
209: } else {
210: support = getSuccessor().getSupportLevel(constraint, query);
211: }
212: return support;
213: }
214:
215: /**
216: * Tests if an Excelibur Text Search index has been made for this field.
217: *
218: * @param field the field.
219: * @return true if an Excelibur Text Search index has been made for this field,
220: * false otherwise.
221: */
222: public boolean hasEtxIndex(StepField field) {
223: boolean result = false;
224: if (field.getType() == Field.TYPE_STRING
225: || field.getType() == Field.TYPE_XML) {
226: result = indexedFields.contains(field.getStep()
227: .getTableName()
228: + "." + field.getFieldName());
229: }
230: return result;
231: }
232:
233: /**
234: * Tests if the query contains additional constraints, i.e. on relations
235: * or nodes.
236: *
237: * @param query the query.
238: * @return true if the query containts additional constraints,
239: * false otherwise.
240: */
241: protected boolean hasAdditionalConstraints(SearchQuery query) {
242: Iterator<Step> iSteps = query.getSteps().iterator();
243: while (iSteps.hasNext()) {
244: Step step = iSteps.next();
245: if (step instanceof RelationStep
246: || step.getNodes().size() > 0) {
247: // Additional constraints on relations or nodes.
248: return true;
249: }
250: }
251: // No additonal constraints:
252: return false;
253: }
254:
255: /**
256: * Tests if a constaint is/contains another stringsearch constraint than
257: * the specified one. Recursively seaches through all childs of composite
258: * constraints.
259: *
260: * @param constraint the constraint.
261: * @param searchConstraint the stringsearch constraint.
262: * @return true if the constraint is/contains another stringsearch constraint
263: * than the given one, false otherwise.
264: */
265: protected boolean containsOtherStringSearchConstraints(
266: Constraint constraint,
267: StringSearchConstraint searchConstraint) {
268: if (constraint instanceof CompositeConstraint) {
269: // Composite constraint.
270: Iterator<Constraint> iChildConstraints = ((CompositeConstraint) constraint)
271: .getChilds().iterator();
272: while (iChildConstraints.hasNext()) {
273: Constraint childConstraint = iChildConstraints.next();
274: if (containsOtherStringSearchConstraints(
275: childConstraint, searchConstraint)) {
276: // Another stringsearch constraint found in childs.
277: return true;
278: }
279: }
280: // No other stringsearch constraint found in childs.
281: return false;
282:
283: } else if (constraint instanceof StringSearchConstraint
284: && constraint != searchConstraint) {
285: // Anther stringsearch constraint.
286: return true;
287:
288: } else {
289: // Not another stringsearch constraint and not a composite.
290: return false;
291: }
292: }
293:
294: /**
295: * Initializes the handler by reading the etxindices configuration file
296: * to determine which fields have a etx index.
297: * <p>
298: * The configurationfile must be named <em>etxindices.xml</em> and located
299: * inside the <em>databases</em> configuration directory.
300: *
301: * @throw IOException When a failure occurred while trying to read the
302: * configuration file.
303: */
304: private void init() throws IOException {
305: File etxConfigFile = new File(MMBaseContext.getConfigPath()
306: + "/databases/etxindices.xml");
307: XmlEtxIndicesReader configReader = new XmlEtxIndicesReader(
308: new InputSource(new BufferedReader(new FileReader(
309: etxConfigFile))));
310:
311: for (Iterator<Element> eSbspaces = configReader
312: .getSbspaceElements(); eSbspaces.hasNext();) {
313: Element sbspace = eSbspaces.next();
314:
315: for (Iterator<Element> eEtxIndices = configReader
316: .getEtxindexElements(sbspace); eEtxIndices
317: .hasNext();) {
318: Element etxIndex = eEtxIndices.next();
319: String table = configReader.getEtxindexTable(etxIndex);
320: String field = configReader.getEtxindexField(etxIndex);
321: String index = configReader.getEtxindexValue(etxIndex);
322: try {
323: String builderField = toBuilderField(table, field);
324: indexedFields.add(builderField);
325: log.service("Registered etx index \"" + index
326: + "\" for builderfield " + builderField);
327: } catch (IllegalArgumentException e) {
328: log.error("Failed to register etx index \"" + index
329: + "\": " + e);
330: }
331: }
332: }
333: }
334:
335: /**
336: * Finds builderfield corresponding to the database table and field names.
337: *
338: * @param dbTable The tablename used in the database.
339: * @param dbField The fieldname used in the database.
340: * @return The corresponding builderfield represented by a string of the
341: * form <buildername>.<fieldname>.
342: * @throws IllegalArgumentException when an invalid argument is supplied.
343: */
344: static String toBuilderField(String dbTable, String dbField) {
345: // package visibility!
346: MMBase mmbase = MMBase.getMMBase();
347: StorageManagerFactory factory = mmbase
348: .getStorageManagerFactory();
349: String tablePrefix = mmbase.getBaseName() + "_";
350:
351: if (!dbTable.startsWith(tablePrefix)) {
352: throw new IllegalArgumentException("Invalid tablename: \""
353: + dbTable + "\". "
354: + "It should start with the prefix \""
355: + tablePrefix + "\".");
356: }
357:
358: String builderName = dbTable.substring(tablePrefix.length());
359: MMObjectBuilder builder;
360: try {
361: builder = mmbase.getBuilder(builderName);
362: } catch (BuilderConfigurationException e) {
363: // Unknown builder.
364: builder = null;
365: }
366:
367: if (builder == null) {
368: throw new IllegalArgumentException("Unknown builder: \""
369: + builderName + "\".");
370: }
371:
372: Iterator<String> iFieldNames = builder.getFieldNames()
373: .iterator();
374: while (iFieldNames.hasNext()) {
375: String fieldName = iFieldNames.next();
376: if (factory.getStorageIdentifier(fieldName).equals(dbField)) {
377: return builderName + "." + fieldName;
378: }
379: }
380:
381: throw new IllegalArgumentException(
382: "No field corresponding to database field \"" + dbField
383: + "\" found in builder \"" + builderName
384: + "\".");
385: }
386:
387: }
|