001: /***************************************************************
002: * This file is part of the [fleXive](R) project.
003: *
004: * Copyright (c) 1999-2008
005: * UCS - unique computing solutions gmbh (http://www.ucs.at)
006: * All rights reserved
007: *
008: * The [fleXive](R) project is free software; you can redistribute
009: * it and/or modify it under the terms of the GNU General Public
010: * License as published by the Free Software Foundation;
011: * either version 2 of the License, or (at your option) any
012: * later version.
013: *
014: * The GNU General Public License can be found at
015: * http://www.gnu.org/copyleft/gpl.html.
016: * A copy is found in the textfile GPL.txt and important notices to the
017: * license from the author are found in LICENSE.txt distributed with
018: * these libraries.
019: *
020: * This library is distributed in the hope that it will be useful,
021: * but WITHOUT ANY WARRANTY; without even the implied warranty of
022: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
023: * GNU General Public License for more details.
024: *
025: * For further information about UCS - unique computing solutions gmbh,
026: * please see the company website: http://www.ucs.at
027: *
028: * For further information about [fleXive](R), please see the
029: * project website: http://www.flexive.org
030: *
031: *
032: * This copyright notice MUST APPEAR in all copies of the file!
033: ***************************************************************/package com.flexive.sqlParser;
034:
035: import com.flexive.shared.exceptions.FxSqlSearchException;
036: import com.flexive.shared.search.query.VersionFilter;
037:
038: import java.io.ByteArrayInputStream;
039: import java.util.ArrayList;
040: import java.util.HashMap;
041: import java.util.List;
042:
043: import org.apache.commons.lang.StringUtils;
044: import org.apache.commons.logging.Log;
045: import org.apache.commons.logging.LogFactory;
046:
047: /**
048: * Statement.
049: *
050: * @author Gregor Schober (gregor.schober@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
051: */
052: public class FxStatement {
053: private static final Log LOG = LogFactory.getLog(FxStatement.class);
054:
055: public static enum Type {
056: /** the statement filter the data */
057: FILTER,
058: /** the statement will always return a empty resultset */
059: EMPTY,
060: /** The statement will always return the whole data from the DB */
061: ALL
062: }
063:
064: private HashMap<String, Table> tables;
065: private Brace currentBrace;
066: private Brace rootBrace;
067: private HashMap<Filter.TYPE, Filter> filters;
068: private boolean bDebug = false;
069: private int iBraceElementIdGenerator = 1;
070: private Type type = Type.FILTER;
071: private ArrayList<SelectedValue> selected;
072: private ArrayList<OrderByValue> order;
073: private int parserExecutionTime = -1;
074: private int maxResultRows = -1;
075: private String cacheKey;
076: private boolean distinct;
077: private boolean ignoreCase = true;
078: private VersionFilter versionFilter = null;
079: private long[] briefcaseFilter = null;
080: private String contentType;
081:
082: protected void setBriefcaseFilter(long[] bf) {
083: briefcaseFilter = bf;
084: }
085:
086: /**
087: * Returns a empty array if the filter is not set, or the id's of all briefcases to search in.
088: *
089: * @return a empty array if the filter is not set, or the id's of all briefcases to search in.
090: */
091: public long[] getBriefcaseFilter() {
092: return briefcaseFilter == null ? new long[0] : briefcaseFilter;
093: }
094:
095: public boolean hasVersionFilter() {
096: return versionFilter != null;
097: }
098:
099: public VersionFilter getVersionFilter() {
100: return versionFilter == null ? VersionFilter.MAX
101: : versionFilter;
102: }
103:
104: public void setVersionFilter(VersionFilter filter) {
105: this .versionFilter = filter;
106: }
107:
108: public boolean getIgnoreCase() {
109: return ignoreCase;
110: }
111:
112: protected void setIgnoreCase(boolean ignoreCase) {
113: this .ignoreCase = ignoreCase;
114: }
115:
116: /**
117: * Gets the maximum rows returned by the search.
118: *
119: * @return the maximum rows returned by the search
120: */
121: public int getMaxResultRows() {
122: return maxResultRows == -1 ? 2000 : maxResultRows; // TODO
123: }
124:
125: /**
126: * Sets the maximum rows returned by the search.
127: *
128: * @param maxResultRows the maximum rows returned by the search
129: */
130: protected void setMaxResultRows(int maxResultRows) {
131: this .maxResultRows = maxResultRows;
132: }
133:
134: /**
135: * Sets the contentname to filter by, may be null to indicate that the filter is not set
136: *
137: * @param contentName the content name
138: */
139: protected void setContentTypeFilter(String contentName) {
140: this .contentType = contentName.trim().length() == 0 ? null
141: : contentName.trim().toUpperCase();
142: }
143:
144: /**
145: * Returns the contentname to filter by, or null if this filter option is not set.
146: *
147: * @return the contentname or null
148: */
149: public String getContentTypeFilter() {
150: return contentType;
151: }
152:
153: /**
154: * Returns true if the content type filter is set.
155: *
156: * @return true if the content type filter is set
157: */
158: public boolean hasContentTypeFilter() {
159: return contentType != null;
160: }
161:
162: /**
163: * Generates a new statement scope unique brace id.
164: *
165: * @return a new brace id
166: */
167: protected int getNewBraceElementId() {
168: return iBraceElementIdGenerator++;
169: }
170:
171: /**
172: * Add a Value to the selected elements.
173: *
174: * @param vi the element
175: * @param alias the alias
176: */
177: protected void addSelectedValue(final Value vi, String alias) {
178: if (alias == null) {
179: if (vi instanceof Property) {
180: final Property property = (Property) vi;
181: alias = property.getValue()
182: + (StringUtils.isNotBlank(property.getField()) ? "."
183: + property.getField()
184: : "");
185: } else {
186: alias = (vi.getValue() instanceof String) ? (String) vi
187: .getValue() : String.valueOf(vi.getValue());
188: }
189: }
190: selected.add(new SelectedValue(vi, alias));
191: }
192:
193: /**
194: * Returns the selected values in the correct order.
195: *
196: * @return the selected values
197: */
198: public List<SelectedValue> getSelectedValues() {
199: return selected;
200: }
201:
202: /**
203: * Returns the selected value matching the given alias, or null if no match can be found.
204: * <p/>
205: * If a alias is used more than one time the first match will be returned.
206: *
207: * @param alias the alias to look for
208: * @return the matching selected value, or null
209: */
210: public SelectedValue getSelectedValueByAlias(String alias) {
211: for (SelectedValue sv : selected) {
212: if (sv.getAlias().equalsIgnoreCase(alias))
213: return sv;
214: }
215: return null;
216: }
217:
218: /**
219: * Overrides the selected values in the given order.
220: *
221: * @param values the new list
222: */
223: public void setSelectedValues(ArrayList<SelectedValue> values) {
224: this .selected = values;
225: }
226:
227: /**
228: * Adds a new order by condition.
229: *
230: * @param vi the order by value (column)
231: * @throws com.flexive.sqlParser.SqlParserException
232: * if the column cannot be used for ordering because it is not selected
233: */
234: public void addOrderByValue(final OrderByValue vi)
235: throws SqlParserException {
236: computeOrderByColumn(vi);
237: order.add(0, vi);
238: }
239:
240: /**
241: * Returns the sort order elements.
242: *
243: * @return the sort order values
244: */
245: public List<OrderByValue> getOrderByValues() {
246: return order;
247: }
248:
249: /**
250: * Returns a table used by the statement by its alias.
251: *
252: * @param alias the alias to look for
253: * @return a table used by the statement by its alias
254: */
255: public Table getTableByAlias(String alias) {
256: return this .tables.get(alias.toUpperCase());
257: }
258:
259: public Table getTableByType(Table.TYPE type) {
260: for (Table tbl : getTables()) {
261: if (tbl.getType() == type)
262: return tbl;
263: }
264: return null;
265: }
266:
267: /**
268: * Returns all tables that were specified in the 'from' section of the statement.
269: *
270: * @return all tables
271: */
272: public Table[] getTables() {
273: Table[] result = new Table[tables.size()];
274: int pos = 0;
275: for (String key : this .tables.keySet()) {
276: result[pos++] = this .tables.get(key);
277: }
278: return result;
279: }
280:
281: /**
282: * Returns the root brace.
283: * <p/>
284: * The root brace will be null if the getType is TYPE.ALL or TYPE.EMPTY
285: *
286: * @return the root brace
287: */
288: public Brace getRootBrace() {
289: return this .rootBrace;
290: }
291:
292: /**
293: * Returns the statements type.
294: * <p/>
295: * TYPE.FILTER: there are conditions<br>
296: * TYPE.EMPTY: the statement will not deliver any results<br>
297: * TYPE.ALL: the statements will deliver all data from the selected sources (no filter set)
298: *
299: * @return the statements type
300: */
301: public Type getType() {
302: return this .type;
303: }
304:
305: /**
306: * Parses a statement.
307: *
308: * @param query the query to process
309: * @return the statement
310: * @throws SqlParserException ifthe function fails
311: */
312: public static FxStatement parseSql(String query)
313: throws SqlParserException {
314: try {
315: long startTime = System.currentTimeMillis();
316: query = cleanupQueryString(query);
317: ByteArrayInputStream byis = new ByteArrayInputStream(query
318: .getBytes("UTF-8"));
319: FxStatement stmt = new SQL(byis, "UTF-8").statement();
320: byis.close();
321: stmt.setParserExecutionTime((int) (System
322: .currentTimeMillis() - startTime));
323: return stmt;
324: } catch (TokenMgrError exc) {
325: throw new SqlParserException(exc, query);
326: } catch (ParseException exc) {
327: throw new SqlParserException(exc, query);
328: } catch (SqlParserException exc) {
329: throw exc;
330: } catch (Exception exc) {
331: throw new SqlParserException(exc.getMessage());
332: }
333: }
334:
335: protected void setParserExecutionTime(int ms) {
336: this .parserExecutionTime = ms;
337: }
338:
339: /**
340: * Returns the execution time needed by the parser in ms.
341: *
342: * @return the execution time needed by the parser in ms
343: */
344: public int getParserExecutionTime() {
345: return this .parserExecutionTime;
346: }
347:
348: // Protected section
349: protected FxStatement() {
350: this .rootBrace = new Brace(this );
351: this .currentBrace = this .rootBrace;
352: this .tables = new HashMap<String, Table>(10);
353: this .filters = new HashMap<Filter.TYPE, Filter>(5);
354: this .selected = new ArrayList<SelectedValue>(50);
355: this .order = new ArrayList<OrderByValue>(5);
356: }
357:
358: protected void addFilter(Filter f) {
359: this .filters.put(f.getType(), f);
360: }
361:
362: /**
363: * Returns the desired filter, or null if the filter was not specified and has no
364: * default value.
365: *
366: * @param t the filter to get
367: * @return the filter
368: */
369: public Filter getFilter(Filter.TYPE t) {
370: return filters.get(t);
371: }
372:
373: protected void addTable(Table table) {
374: if (bDebug)
375: System.out.println("Adding table: " + table);
376: this .tables.put(table.getAlias(), table);
377: }
378:
379: protected Brace getCurrentBrace() {
380: return this .currentBrace;
381: }
382:
383: protected Brace startSubBrace() throws SqlParserException {
384: Brace br = new Brace(this );
385: currentBrace.addElement(br);
386: currentBrace = br;
387: return currentBrace;
388: }
389:
390: protected Brace endSubBrace() throws SqlParserException {
391: Brace parent = currentBrace.getParent();
392: if (currentBrace.size() == 1) {
393: // Brace with only one element, remove it and move its element to the parent
394: BraceElement ele = currentBrace.removeLastElement();
395: parent.removeElement(currentBrace);
396: parent.addElement(ele);
397: }
398: currentBrace = parent;
399: return currentBrace;
400: }
401:
402: /**
403: * Removes empty braces and handles conditions that are always false or true.
404: * Also checks if all aliases are defined.
405: *
406: * @throws SqlParserException if the function fails
407: */
408: protected void cleanup() throws SqlParserException {
409:
410: // Check if all referenced tables are present
411: for (SelectedValue val : selected) {
412: if (!(val.getValue() instanceof Property))
413: continue;
414: Property prop = (Property) val.getValue();
415: if (this .getTableByAlias(prop.getTableAlias()) == null) {
416: String stables = "";
417: for (String alias : tables.keySet()) {
418: stables += ((stables.length() > 0) ? "," : "")
419: + alias;
420: }
421: throw new SqlParserException(
422: "ex.sqlSearch.filter.unknownTableAlias", prop
423: .getTableAlias(), stables);
424: }
425: }
426:
427: // Check order by
428: for (OrderByValue ov : getOrderByValues()) {
429: computeOrderByColumn(ov);
430: }
431:
432: // If no where clause is set at all
433: if (this .rootBrace == null || this .rootBrace.size() == 0) {
434: rootBrace = null;
435: type = Type.ALL;
436: return;
437: }
438:
439: // Cleanup where clause
440: cleanup(this .rootBrace);
441:
442: if (rootBrace != null) {
443: // Nothing left and TYPE=FILTER has to be set to TYPE.EMPTY
444: if (this .rootBrace.size() == 0
445: && this .getType() == Type.FILTER) {
446: this .type = Type.EMPTY;
447: this .rootBrace = null;
448: }
449: // Only one condition at top level: type has to be null and not 'or'/'and'
450: else if (this .rootBrace.size() == 1) {
451: this .rootBrace.setType(null);
452: }
453: }
454: }
455:
456: private void computeOrderByColumn(OrderByValue ov)
457: throws SqlParserException {
458: if (StringUtils.isNumeric(ov.getValue())) {
459: // column selected by index (1-based)
460: final int index = Integer.valueOf(ov.getValue()) - 1;
461: if (index >= 0 && selected.size() > index) {
462: ov.setSelectedValue(selected.get(index), index);
463: } else {
464: throw new SqlParserException(
465: "ex.sqlSearch.invalidOrderByIndex", ov
466: .getValue(), selected.size());
467: }
468: } else {
469: // column selected by alias
470: boolean found = false;
471: int pos = 0;
472: for (SelectedValue sv : selected) {
473: if (sv.getAlias().equalsIgnoreCase(ov.getValue())) {
474: found = true;
475: ov.setSelectedValue(sv, pos);
476: break;
477: }
478: pos++;
479: }
480: if (!found) {
481: throw new SqlParserException(
482: "ex.sqlSearch.invalidOrderByValue", ov
483: .getValue());
484: }
485: }
486: }
487:
488: /**
489: * Compute a cache key for the statement.
490: * <p/>
491: * Statements with the same cache key will produce the same resultset.
492: *
493: * @return the cacheKey.
494: * @throws SqlParserException if the cache key could not be computed
495: */
496: protected String getCacheKey() throws SqlParserException {
497: try {
498: // Only build once
499: if (cacheKey != null) {
500: return cacheKey;
501: }
502:
503: StringBuffer key = new StringBuffer(512);
504:
505: if (type == Type.FILTER) {
506: computeCacheKey(key, this .rootBrace);
507: } else {
508: key.append(type);
509: }
510:
511: for (String table : this .tables.keySet()) {
512: Table t = this .tables.get(table);
513: Filter vf = t.getFilter(Filter.TYPE.VERSION);
514: String langs = "";
515: for (String lang : t.getSearchLanguages()) {
516: langs += "|" + lang;
517: }
518: key.append("_").append(t.getAlias()).append(";")
519: .append(t.getType()).append(";").append(langs)
520: .append(";").append(
521: vf == null ? "null" : vf.getValue());
522: }
523: key.append("_").append("_").append(this .getMaxResultRows())
524: .append("_").append(this .isDistinct() ? "D" : "A");
525:
526: cacheKey = key.toString();
527: return cacheKey;
528: } catch (Throwable t) {
529: System.err.println(t.getMessage());
530: t.printStackTrace();
531: throw new SqlParserException(
532: "ex.sqlSearch.unbableToBuildCachekey", t);
533: }
534: }
535:
536: /**
537: * Sets the distinct condition of the statement.
538: *
539: * @param value the condition
540: */
541: protected void setDistinct(boolean value) {
542: this .distinct = value;
543: }
544:
545: /**
546: * Returns if the statements resultset is distinct.
547: *
548: * @return true if the statements resultset is distinct
549: */
550: public boolean isDistinct() {
551: return this .distinct;
552: }
553:
554: /**
555: * Helper function for computeCacheKey (recursion).
556: *
557: * @param sb the string buffer to write to
558: * @param br the current brace
559: */
560: private void computeCacheKey(StringBuffer sb, Brace br) {
561: String brType = (br.getType() == null ? "N" : (br.getType()
562: .equalsIgnoreCase("and") ? "A" : "O"));
563: sb.append("(").append(brType);
564: for (BraceElement be : br.getElements()) {
565: if (be instanceof Condition) {
566: sb.append((" " + be.toString()));
567: } else {
568: computeCacheKey(sb, (Brace) be);
569: }
570: }
571: sb.append(")");
572: }
573:
574: private void removeWholeBrace(Brace br, boolean alwaysTrue) {
575: try {
576: if (bDebug)
577: System.out
578: .println("Removing whole brace because of always "
579: + alwaysTrue + " cond");
580: } catch (Exception exc) {
581: System.err.println("###Y" + exc.getMessage());
582: }
583:
584: if (br.getParent() == null) {
585: br.removeAllElements();
586: rootBrace = null;
587: type = alwaysTrue ? Type.ALL : Type.EMPTY;
588: } else {
589: Brace parent = br.getParent();
590: try {
591: Condition cd = new Condition(this , new Constant("1"),
592: Condition.Comparator.EQUAL, new Constant(
593: alwaysTrue ? "1" : "0"));
594: parent.addElement(cd);
595: } catch (Exception exc) {
596: System.err.println("###Y" + exc.getMessage());
597: }
598: parent.removeElement(br);
599: }
600: }
601:
602: /**
603: * Perform an cleanup on the statement.
604: *
605: * @param br the brace to work on
606: * @throws SqlParserException if the function fails
607: */
608: private void cleanup(Brace br) throws SqlParserException {
609:
610: // Move down the brace tree (recursive) ..
611: for (BraceElement be : br.getElements()) {
612: if (be instanceof Brace) {
613: cleanup((Brace) be);
614: }
615: }
616:
617: // ... now start cleanup from bottom up
618: if (br.isOr()) {
619: for (BraceElement be : br.getElements()) {
620: if (!(be instanceof Condition))
621: continue;
622: Condition cond = (Condition) be;
623: if (cond.isAlwaysTrue()) {
624: // Whole brace will be true, remove it and terminate loop
625: removeWholeBrace(br, true);
626: break;
627: }
628: if (cond.isAlwaysFalse()) {
629: // Condition always false, so remove it
630: br.removeElement(cond);
631: }
632: }
633: // If the whole OR is empty we need to add an ALWAYS false to the parent
634: if (br.size() == 0 && br.getParent() != null) {
635: removeWholeBrace(br, false);
636: }
637:
638: } else if (br.isAnd()) {
639: for (BraceElement be : br.getElements()) {
640: if (!(be instanceof Condition))
641: continue;
642: Condition cond = (Condition) be;
643: if (cond.isAlwaysTrue()) {
644: // Condition always true, so remove it
645: br.removeElement(cond);
646: }
647: if (cond.isAlwaysFalse()) {
648: // Whole brace will be false, remove it and terminate loop
649: removeWholeBrace(br, false);
650: break;
651: }
652: }
653: // If the whole AND is empty we need to add an ALWAYS true to the parent
654: if (br.size() == 0 && br.getParent() != null) {
655: removeWholeBrace(br, true);
656: }
657: } else if (br.getSize() > 0) {
658: // Just one single condition is set, examine it!
659: BraceElement be = br.getElementAt(0);
660: if (be instanceof Condition) {
661: Condition cond = (Condition) be;
662: if (cond.isAlwaysTrue()) {
663: // Everything is selected!
664: br.removeAllElements();
665: this .rootBrace = null;
666: this .type = Type.ALL;
667: } else if (cond.isAlwaysFalse()) {
668: // No result at all!
669: br.removeAllElements();
670: this .rootBrace = null;
671: this .type = Type.EMPTY;
672: }
673: }
674: }
675:
676: // Cleanup empty braces, and braces that contain just one element
677: if (br.size() == 1) {
678: Brace parent = br.getParent();
679: if (parent != null) {
680: BraceElement be = br.removeLastElement();
681: if (bDebug)
682: System.out
683: .println("Moving element to parent since its the only one left in the brace: "
684: + be);
685: parent.removeElement(br);
686: parent.addElement(be);
687: } else {
688: // Only one brace in root brace -> make it the root brace
689: if (br.getElementAt(0) instanceof Brace) {
690: rootBrace = (Brace) br.removeLastElement();
691: }
692: }
693: } else if (br.size() == 0 && br.getParent() != null) {
694: if (bDebug)
695: System.out.println("Removing empty brace: " + br);
696: br.getParent().removeElement(br);
697: }
698: }
699:
700: /**
701: * Generates a debug string.
702: *
703: * @return a debug string of the statement
704: * @throws com.flexive.shared.exceptions.FxSqlSearchException
705: * if the function failed
706: */
707: public String printDebug() throws FxSqlSearchException {
708: try {
709: StringBuffer result = new StringBuffer(1024);
710: if (this .rootBrace == null) {
711: return String.valueOf(this .getType());
712: } else {
713: printDebug(result, this .rootBrace);
714: result = result.deleteCharAt(0);
715: }
716: result
717: .append(("\n##################################################\n"));
718: int pos = 0;
719: for (SelectedValue v : getSelectedValues()) {
720: result.append(("Selected[" + (pos++) + "]: "
721: + v.toString() + "\n"));
722: }
723:
724: pos = 0;
725: result.append("Order by: ");
726: for (Value v : getOrderByValues()) {
727: if ((pos++) > 0)
728: result.append(",");
729: result.append(v.toString());
730: }
731: if (pos == 0) {
732: result.append(("Order: n/a\n"));
733: }
734: result.append("\n");
735:
736: result.append("Search Languages: ");
737: pos = 0;
738: for (String v : getTableByType(Table.TYPE.CONTENT)
739: .getSearchLanguages()) {
740: if ((pos++) > 0)
741: result.append(",");
742: result.append(v);
743: }
744: result.append("\n");
745: result.append("Cache Key: ");
746: try {
747: result.append(getCacheKey());
748: } catch (Throwable t) {
749: result.append(t.getMessage());
750: }
751: result.append("\n");
752: result.append(("Parser execution time: "
753: + this .getParserExecutionTime() + " ms\n"));
754: return result.toString();
755: } catch (Throwable t) {
756: throw new FxSqlSearchException(LOG, t,
757: "ex.sqlSearch.printDebugFailed");
758: }
759: }
760:
761: /**
762: * Generates a debug string for the statement.
763: *
764: * @param sb the StringBuffer to write to
765: * @param br the current bracer (recursion)
766: */
767: protected void printDebug(StringBuffer sb, Brace br) {
768: String sPrefix = "\n";
769: for (int i = 0; i < br.getLevel(); i++) {
770: sPrefix += "+";
771: }
772: sPrefix += (br.getType() == null ? "N" : (br.getType()
773: .equalsIgnoreCase("and") ? "A" : "O"))
774: + " ";
775: sb.append((sPrefix + "("));
776: boolean hadSubs = false;
777: for (BraceElement be : br.getElements()) {
778: if (be instanceof Condition) {
779: sb.append((" " + be.toString() + " "));
780: } else {
781: printDebug(sb, (Brace) be);
782: hadSubs = true;
783: }
784: }
785: if (hadSubs) {
786: sb.append((sPrefix + ")"));
787: } else {
788: sb.append(" )");
789: }
790: }
791:
792: // Private section
793:
794: /**
795: * Cleanup the query string.
796: * <p/>
797: * This function removes comments from the query.
798: *
799: * @param query the query to cleanup
800: * @return the processed query string
801: * @throws SqlParserException ifthe function fails
802: */
803: private static String cleanupQueryString(String query)
804: throws SqlParserException {
805: // Make sure the statement has the EOF charater at the end, also add one
806: // '\n' to ensure that the last line comment ("--") is closed
807: query = query.trim();
808: query += "\n";
809: if (query.charAt(query.length() - 1) != ';')
810: query += ";";
811: return query;
812: }
813:
814: }
|