001: /*
002: This software is OSI Certified Open Source Software.
003: OSI Certified is a certification mark of the Open Source Initiative.
004:
005: The license (Mozilla version 1.0) can be read at the MMBase site.
006: See http://www.MMBase.org/license
007:
008: */
009: package org.mmbase.storage.search.implementation.database;
010:
011: import java.sql.*;
012: import java.util.*;
013: import javax.sql.DataSource;
014:
015: import org.mmbase.cache.*;
016: import org.mmbase.bridge.Field;
017: import org.mmbase.bridge.NodeManager;
018: import org.mmbase.core.CoreField;
019: import org.mmbase.module.core.*;
020: import org.mmbase.module.database.MultiConnection;
021: import org.mmbase.storage.implementation.database.DatabaseStorageManager;
022: import org.mmbase.storage.implementation.database.DatabaseStorageManagerFactory;
023: import org.mmbase.storage.search.*;
024: import org.mmbase.util.logging.*;
025: import org.mmbase.storage.search.implementation.ModifiableQuery;
026:
027: /**
028: * Basic implementation using a database.
029: * Uses a {@link org.mmbase.storage.search.implementation.database.SqlHandler SqlHandler}
030: * to create SQL string representations of search queries.
031: * <p>
032: * In order to execute search queries, these are represented as SQL strings
033: * by the handler, and in this form executed on the database.
034: *
035: * @author Rob van Maris
036: * @version $Id: BasicQueryHandler.java,v 1.62 2007/10/22 08:42:40 nklasens Exp $
037: * @since MMBase-1.7
038: */
039: public class BasicQueryHandler implements SearchQueryHandler {
040:
041: /** Empty StepField array. */
042: private static final StepField[] STEP_FIELD_ARRAY = new StepField[0];
043:
044: private static final Logger log = Logging
045: .getLoggerInstance(BasicQueryHandler.class);
046:
047: /** Sql handler used to generate SQL statements. */
048: private SqlHandler sqlHandler = null;
049:
050: /** MMBase instance. */
051: private MMBase mmbase = null;
052:
053: /**
054: * Default constructor.
055: *
056: * @param sqlHandler The handler use to create SQL string representations
057: * of search queries.
058: */
059: public BasicQueryHandler(SqlHandler sqlHandler) {
060: this .sqlHandler = sqlHandler;
061: mmbase = MMBase.getMMBase();
062: }
063:
064: // javadoc is inherited
065: public List<MMObjectNode> getNodes(SearchQuery query,
066: MMObjectBuilder builder) throws SearchQueryException {
067:
068: List<MMObjectNode> results;
069: Connection con = null;
070: PreparedStatement stmt = null;
071: String sqlString = null;
072:
073: try {
074: // Flag, set if offset must be supported by skipping results.
075: boolean mustSkipResults = (query.getOffset() != SearchQuery.DEFAULT_OFFSET)
076: && (sqlHandler.getSupportLevel(
077: SearchQueryHandler.FEATURE_OFFSET, query) == SearchQueryHandler.SUPPORT_NONE);
078:
079: // Flag, set if sql handler supports maxnumber.
080: boolean sqlHandlerSupportsMaxNumber = sqlHandler
081: .getSupportLevel(
082: SearchQueryHandler.FEATURE_MAX_NUMBER,
083: query) != SearchQueryHandler.SUPPORT_NONE;
084:
085: // report about offset and max support (for debug purposes)
086: if (log.isDebugEnabled()) {
087: log
088: .debug("Database offset support = "
089: + (sqlHandler
090: .getSupportLevel(
091: SearchQueryHandler.FEATURE_OFFSET,
092: query) != SearchQueryHandler.SUPPORT_NONE));
093: log.debug("mustSkipResults = " + mustSkipResults);
094: log.debug("Database max support = "
095: + sqlHandlerSupportsMaxNumber);
096: }
097:
098: sqlString = createSqlString(query, mustSkipResults,
099: sqlHandlerSupportsMaxNumber);
100:
101: if (log.isDebugEnabled()) {
102: log.debug("sql: " + sqlString);
103: }
104:
105: // Execute the SQL... ARGH !!! Has to move!
106: // get connection...
107: DataSource dataSource = ((DatabaseStorageManagerFactory) mmbase
108: .getStorageManagerFactory()).getDataSource();
109: con = dataSource.getConnection();
110: ResultSet rs = null;
111: try {
112: stmt = con.prepareStatement(sqlString);
113: rs = stmt.executeQuery();
114: if (mustSkipResults) {
115: log
116: .debug("skipping results, to provide weak support for offset");
117: for (int i = 0; i < query.getOffset(); i++) {
118: rs.next();
119: }
120: }
121:
122: // Now store results as cluster-/real nodes.
123: StepField[] fields = query.getFields().toArray(
124: STEP_FIELD_ARRAY);
125: int maxNumber = query.getMaxNumber();
126:
127: // now, we dispatch the reading of the result set to the right function wich instantiates Nodes of the right type.
128: if (builder instanceof ClusterBuilder) {
129: results = readNodes((ClusterBuilder) builder,
130: fields, rs, sqlHandlerSupportsMaxNumber,
131: maxNumber, query.getSteps().size());
132: } else if (builder instanceof ResultBuilder) {
133: results = readNodes((ResultBuilder) builder,
134: fields, rs, sqlHandlerSupportsMaxNumber,
135: maxNumber);
136: } else {
137: results = readNodes(builder, fields, rs,
138: sqlHandlerSupportsMaxNumber, maxNumber);
139: }
140: } finally {
141: if (rs != null) {
142: rs.close();
143: }
144: }
145: } catch (SQLException e) {
146: // Something went wrong, log exception
147: // and rethrow as SearchQueryException.
148: if (log.isDebugEnabled()) {
149: log.debug("Query failed:" + query + "\n" + e
150: + Logging.stackTrace(e));
151: }
152: if (con instanceof MultiConnection) {
153: log.debug("Calling check after exception");
154: try {
155: ((MultiConnection) con).checkAfterException();
156: } catch (SQLException sqe) {
157: log.debug(sqe);
158: }
159: } else {
160: log.debug("Not a multiconnection");
161: }
162: throw new SearchQueryException("Query '"
163: + (sqlString == null ? "" + query.toString()
164: : sqlString) + "' failed: "
165: + e.getClass().getName() + ": " + e.getMessage(), e);
166: } finally {
167: closeConnection(con, stmt);
168: }
169:
170: return results;
171: }
172:
173: /**
174: * Safely close a database connection and/or a database statement.
175: * @param con The connection to close. Can be <code>null</code>.
176: * @param stmt The statement to close, prior to closing the connection. Can be <code>null</code>.
177: */
178: protected void closeConnection(Connection con, Statement stmt) {
179: try {
180: if (stmt != null) {
181: stmt.close();
182: }
183: } catch (Exception g) {
184: }
185: try {
186: if (con != null) {
187: con.close();
188: }
189: } catch (Exception g) {
190: }
191: }
192:
193: /**
194: * Makes a String of a query, taking into consideration if the database supports offset and
195: * maxnumber features. The resulting String is an SQL query which can be fed to the database.
196: * @param query the query to convert to sql
197: * @return the sql string
198: * @throws SearchQueryException when error occurs while making the string
199: */
200: public String createSqlString(SearchQuery query)
201: throws SearchQueryException {
202: // Flag, set if offset must be supported by skipping results.
203: boolean mustSkipResults = (query.getOffset() != SearchQuery.DEFAULT_OFFSET)
204: && (sqlHandler.getSupportLevel(
205: SearchQueryHandler.FEATURE_OFFSET, query) == SearchQueryHandler.SUPPORT_NONE);
206:
207: // Flag, set if sql handler supports maxnumber.
208: boolean sqlHandlerSupportsMaxNumber = sqlHandler
209: .getSupportLevel(SearchQueryHandler.FEATURE_MAX_NUMBER,
210: query) != SearchQueryHandler.SUPPORT_NONE;
211:
212: // report about offset and max support (for debug purposes)
213: if (log.isDebugEnabled()) {
214: log
215: .debug("Database offset support = "
216: + (sqlHandler.getSupportLevel(
217: SearchQueryHandler.FEATURE_OFFSET,
218: query) != SearchQueryHandler.SUPPORT_NONE));
219: log.debug("mustSkipResults = " + mustSkipResults);
220: log.debug("Database max support = "
221: + sqlHandlerSupportsMaxNumber);
222: }
223:
224: return createSqlString(query, mustSkipResults,
225: sqlHandlerSupportsMaxNumber);
226: }
227:
228: /**
229: * Makes a String of a query, taking into consideration if the database supports offset and
230: * maxnumber features. The resulting String is an SQL query which can be fed to the database.
231: */
232:
233: private String createSqlString(SearchQuery query,
234: boolean mustSkipResults, boolean sqlHandlerSupportsMaxNumber)
235: throws SearchQueryException {
236: int maxNumber = query.getMaxNumber();
237: // Flag, set if maxnumber must be supported by truncating results.
238: boolean mustTruncateResults = (maxNumber != SearchQuery.DEFAULT_MAX_NUMBER)
239: && (!sqlHandlerSupportsMaxNumber);
240: String sqlString;
241: if (mustSkipResults) { // offset not supported, but needed
242: log
243: .debug("offset used in query and not supported in database.");
244: ModifiableQuery modifiedQuery = new ModifiableQuery(query);
245: modifiedQuery.setOffset(SearchQuery.DEFAULT_OFFSET);
246:
247: if (mustTruncateResults) {
248: log
249: .debug("max used in query but not supported in database.");
250: // Weak support for offset, weak support for maxnumber:
251: modifiedQuery
252: .setMaxNumber(SearchQuery.DEFAULT_MAX_NUMBER); // apply no maximum, but truncate result
253: } else if (maxNumber != SearchQuery.DEFAULT_MAX_NUMBER) {
254: log
255: .debug("max used in query and supported by database.");
256: // Because offset is not supported add max with the offset.
257: // Weak support for offset, sql handler supports maxnumber:
258: modifiedQuery.setMaxNumber(query.getOffset()
259: + maxNumber);
260: }
261: sqlString = sqlHandler.toSql(modifiedQuery, sqlHandler);
262:
263: } else {
264: log
265: .debug("offset not used or offset is supported by the database.");
266: if (mustTruncateResults) {
267: log
268: .debug("max used in query but not supported in database.");
269: // Sql handler supports offset, or not offset is specified.
270: // weak support for maxnumber:
271: ModifiableQuery modifiedQuery = new ModifiableQuery(
272: query);
273: modifiedQuery
274: .setMaxNumber(SearchQuery.DEFAULT_MAX_NUMBER); // apply no maximum, but truncate result
275: sqlString = sqlHandler.toSql(modifiedQuery, sqlHandler);
276: } else {
277: // Offset not used, maxnumber not used.
278: log.debug("no need for modifying Query");
279: sqlString = sqlHandler.toSql(query, sqlHandler);
280: }
281: }
282: // TODO: test maximum sql statement length is not exceeded.
283: return sqlString;
284: }
285:
286: /**
287: * Read the result list and creates a List of ClusterNodes.
288: */
289: private List<MMObjectNode> readNodes(ClusterBuilder builder,
290: StepField[] fields, ResultSet rs,
291: boolean sqlHandlerSupportsMaxNumber, int maxNumber,
292: int numberOfSteps) {
293: List<MMObjectNode> results = new ArrayList<MMObjectNode>();
294: DatabaseStorageManager storageManager = (DatabaseStorageManager) mmbase
295: .getStorageManager();
296:
297: // Truncate results to provide weak support for maxnumber.
298: try {
299: while (rs.next()
300: && (results.size() < maxNumber || maxNumber == -1)) {
301: try {
302: ClusterNode node = new ClusterNode(builder,
303: numberOfSteps);
304: node.start();
305:
306: int j = 1;
307: // make use of Node-cache to fill fields
308: // especially XML-fields can be heavy, otherwise (Documnents must be instantiated)
309: for (StepField element : fields) {
310: String fieldName = element.getFieldName(); // why not getAlias first?
311: Step step = element.getStep();
312: String alias = step.getAlias();
313: if (alias == null) {
314: // Use tablename as alias when no alias is specified.
315: alias = step.getTableName();
316: }
317: CoreField field = builder.getField(alias + '.'
318: + fieldName);
319: if (field.getType() == Field.TYPE_BINARY)
320: continue;
321: Object value = storageManager.getValue(rs, j++,
322: field, false);
323: node.storeValue(alias + '.' + fieldName, value);
324: }
325: node.clearChanged();
326: node.finish();
327: results.add(node);
328: } catch (Exception e) {
329: // log error, but continue with other nodes
330: log.error(e.getMessage(), e);
331: }
332: }
333: } catch (SQLException sqe) {
334: // log error, but return results.
335: log.error(sqe);
336: }
337: return results;
338: }
339:
340: /**
341: * Read the result list and creates a List of ResultNodes
342: */
343: private List<MMObjectNode> readNodes(ResultBuilder builder,
344: StepField[] fields, ResultSet rs,
345: boolean sqlHandlerSupportsMaxNumber, int maxNumber) {
346: List<MMObjectNode> results = new ArrayList<MMObjectNode>();
347: DatabaseStorageManager storageManager = (DatabaseStorageManager) mmbase
348: .getStorageManager();
349:
350: // Truncate results to provide weak support for maxnumber.
351: try {
352: while (rs.next()
353: && (maxNumber > results.size() || maxNumber == -1)) {
354: try {
355: ResultNode node = new ResultNode(builder);
356: node.start();
357: int j = 1;
358: for (StepField element : fields) {
359: String fieldName = element.getAlias();
360: if (fieldName == null) {
361: fieldName = element.getFieldName();
362: }
363: CoreField field = builder.getField(fieldName);
364: if (field != null
365: && field.getType() == Field.TYPE_BINARY)
366: continue;
367: Object value = storageManager.getValue(rs, j++,
368: field, false);
369: node.storeValue(fieldName, value);
370: }
371: node.clearChanged();
372: node.finish();
373: results.add(node);
374: } catch (Exception e) {
375: // log error, but continue with other nodes
376: log.error(e.getMessage(), e);
377: }
378: }
379: } catch (SQLException sqe) {
380: // log error, but return results.
381: log.error(sqe);
382: }
383: return results;
384: }
385:
386: /**
387: * Read the result list and creates a List of normal MMObjectNodes.
388: */
389: private List<MMObjectNode> readNodes(MMObjectBuilder builder,
390: StepField[] fields, ResultSet rs,
391: boolean sqlHandlerSupportsMaxNumber, int maxNumber) {
392: List<MMObjectNode> results = new ArrayList<MMObjectNode>();
393: DatabaseStorageManager storageManager = (DatabaseStorageManager) mmbase
394: .getStorageManager();
395:
396: boolean storesAsFile = builder
397: .getMMBase()
398: .getStorageManagerFactory()
399: .hasOption(
400: org.mmbase.storage.implementation.database.Attributes.STORES_BINARY_AS_FILE);
401: // determine indices of queried fields
402: Map<CoreField, Integer> fieldIndices = new HashMap<CoreField, Integer>();
403: Step nodeStep = fields[0].getStep();
404: int j = 1;
405: for (StepField element : fields) {
406: if (element.getType() == Field.TYPE_BINARY)
407: continue;
408: Integer index = Integer.valueOf(j++);
409: if (element.getStep() == nodeStep) {
410: String fieldName = element.getFieldName();
411: CoreField field = builder.getField(fieldName);
412: if (field == null) {
413: log.warn("Did not find the field '" + fieldName
414: + "' in builder " + builder);
415: continue; // could this happen?
416: }
417: fieldIndices.put(field, index);
418: }
419: }
420:
421: // Test if ALL fields are queried
422: StringBuilder missingFields = null;
423: for (CoreField field : builder
424: .getFields(NodeManager.ORDER_CREATE)) {
425: if (field.inStorage()) {
426: if (field.getType() == Field.TYPE_BINARY)
427: continue;
428: if (fieldIndices.get(field) == null) {
429: if (missingFields == null) {
430: missingFields = new StringBuilder(field
431: .getName());
432: } else {
433: missingFields.append(", ").append(
434: field.getName());
435: }
436: }
437: }
438: }
439:
440: // if not all field are queried, this is a virtual node
441: boolean isVirtual = missingFields != null;
442: if (isVirtual) {
443: log
444: .warn("This query returns virtual nodes (not querying: '"
445: + missingFields + "')");
446: }
447:
448: // Truncate results to provide weak support for maxnumber.
449: try {
450: NodeCache nodeCache = NodeCache.getCache();
451: Cache<Integer, Integer> typeCache = CacheManager
452: .getCache("TypeCache");
453: int builderType = builder.getObjectType();
454: Integer oTypeInteger = Integer.valueOf(builderType);
455: while (rs.next()
456: && (maxNumber > results.size() || maxNumber == -1)) {
457: try {
458: /*
459: * This while statement does not deal with mmbase inheritance
460: * It creates nodes based on the builder passed in. Nodes with
461: * subtypes of this builder are only filled with the field values
462: * of this builder. Builders of a subtype are not stored in the nodeCache
463: * to limit the time scope of these nodes, because they are not complete.
464: */
465:
466: MMObjectNode node;
467: if (!isVirtual) {
468: node = new MMObjectNode(builder, false);
469: } else {
470: node = new VirtualNode(builder);
471: }
472: node.start();
473: for (CoreField field : builder
474: .getFields(NodeManager.ORDER_CREATE)) {
475: if (!field.inStorage())
476: continue;
477: Integer index = fieldIndices.get(field);
478: Object value = null;
479: String fieldName = field.getName();
480: if (index != null) {
481: value = storageManager.getValue(rs, index
482: .intValue(), field, true);
483: } else {
484: java.sql.Blob b = null;
485: if (field.getType() == Field.TYPE_BINARY
486: && storesAsFile) {
487: log
488: .debug("Storage did not return data for '"
489: + fieldName
490: + "', supposing it on disk");
491: // must have been a explicitely specified 'blob' field
492: b = storageManager.getBlobValue(node,
493: field, true);
494: } else if (field.getType() == Field.TYPE_BINARY) {
495: // binary fields never come directly from the database
496: value = MMObjectNode.VALUE_SHORTED;
497: } else if (!isVirtual) {
498: // field wasn't returned by the db - this must be a Virtual node, otherwise fail!
499: // (this shoudln't occur)
500: throw new IllegalStateException(
501: "Storage did not return data for field '"
502: + fieldName + "'");
503: }
504: if (b != null) {
505: if (b.length() == -1) {
506: value = MMObjectNode.VALUE_SHORTED;
507: } else {
508: value = b.getBytes(0L, (int) b
509: .length());
510: }
511: }
512: }
513: node.storeValue(fieldName, value);
514: }
515: node.clearChanged();
516: node.finish();
517:
518: // The following code fills the type- and node-cache as far as this is possible at this stage.
519: // (provided the node is persistent)
520: if (!isVirtual) {
521: int otype = node.getOType();
522: Integer number = Integer.valueOf(node
523: .getNumber());
524: if (otype == builderType) {
525: MMObjectNode cacheNode = nodeCache
526: .get(number);
527: if (cacheNode != null) {
528: node = cacheNode;
529: } else {
530: nodeCache.put(number, node);
531: }
532: typeCache.put(number, oTypeInteger);
533: } else {
534: typeCache.put(number, Integer
535: .valueOf(otype));
536: }
537: }
538:
539: results.add(node);
540: } catch (Exception e) {
541: // log error, but continue with other nodes
542: log.error(e.getMessage(), e);
543: }
544: }
545: } catch (SQLException sqe) {
546: // log error, but return results.
547: log.error(sqe);
548: }
549: return results;
550: }
551:
552: // javadoc is inherited
553: public int getSupportLevel(int feature, SearchQuery query)
554: throws SearchQueryException {
555: int supportLevel;
556: switch (feature) {
557: case SearchQueryHandler.FEATURE_OFFSET:
558: // When sql handler does not support OFFSET, this query handler
559: // provides weak support by skipping resultsets.
560: // (falls through)
561: case SearchQueryHandler.FEATURE_MAX_NUMBER:
562: // When sql handler does not support MAX NUMBER, this query
563: // handler provides weak support by truncating resultsets.
564: int handlerSupport = sqlHandler.getSupportLevel(feature,
565: query);
566: if (handlerSupport == SearchQueryHandler.SUPPORT_NONE) {
567: // TODO: implement weak support.
568: //supportLevel = SearchQueryHandler.SUPPORT_WEAK;
569: supportLevel = SearchQueryHandler.SUPPORT_NONE;
570: } else {
571: supportLevel = handlerSupport;
572: }
573: break;
574:
575: default:
576: supportLevel = sqlHandler.getSupportLevel(feature, query);
577: }
578: return supportLevel;
579: }
580:
581: // javadoc is inherited
582: public int getSupportLevel(Constraint constraint, SearchQuery query)
583: throws SearchQueryException {
584: return sqlHandler.getSupportLevel(constraint, query);
585: }
586:
587: }
|