Java tutorial
/*************************************************************** * This file is part of the [fleXive](R) framework. * * Copyright (c) 1999-2014 * UCS - unique computing solutions gmbh (http://www.ucs.at) * All rights reserved * * The [fleXive](R) project is free software; you can redistribute * it and/or modify it under the terms of the GNU Lesser General Public * License version 2.1 or higher as published by the Free Software Foundation. * * The GNU Lesser General Public License can be found at * http://www.gnu.org/licenses/lgpl.html. * A copy is found in the textfile LGPL.txt and important notices to the * license from the author are found in LICENSE.txt distributed with * these libraries. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * For further information about UCS - unique computing solutions gmbh, * please see the company website: http://www.ucs.at * * For further information about [fleXive](R), please see the * project website: http://www.flexive.org * * * This copyright notice MUST APPEAR in all copies of the file! ***************************************************************/ package com.flexive.sqlParser; import com.flexive.shared.exceptions.FxSqlSearchException; import com.flexive.shared.interfaces.SearchEngine; import com.flexive.shared.search.query.VersionFilter; import com.google.common.base.Charsets; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Statement. * * @author Gregor Schober (gregor.schober@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at) */ public class FxStatement { private static final Log LOG = LogFactory.getLog(FxStatement.class); public static enum Type { /** the statement filter the data */ FILTER, /** the statement will always return a empty resultset */ EMPTY, /** The statement will always return the whole data from the DB */ ALL } private HashMap<String, Table> tables; private Brace currentBrace; private Brace rootBrace; private Map<Filter.TYPE, Filter> filters; private boolean debug = false; private int braceElementIdGenerator = 1; private Type type = Type.FILTER; private List<SelectedValue> selected; private List<OrderByValue> order; private int parserExecutionTime = -1; private int maxResultRows = -1; private String cacheKey; private boolean distinct; private boolean ignoreCase = true; private VersionFilter versionFilter = null; private long[] briefcaseFilter = null; private String contentType; protected FxStatement() throws SqlParserException { this.rootBrace = new Brace(this); this.currentBrace = this.rootBrace; this.tables = new HashMap<String, Table>(10); this.filters = new HashMap<Filter.TYPE, Filter>(5); this.selected = new ArrayList<SelectedValue>(50); this.order = new ArrayList<OrderByValue>(5); addTable(new Table("content", "co")); } protected void setBriefcaseFilter(long[] bf) { briefcaseFilter = bf; } /** * Returns a empty array if the filter is not set, or the id's of all briefcases to search in. * * @return a empty array if the filter is not set, or the id's of all briefcases to search in. */ public long[] getBriefcaseFilter() { return briefcaseFilter == null ? new long[0] : briefcaseFilter; } public boolean hasVersionFilter() { return versionFilter != null; } public VersionFilter getVersionFilter() { return versionFilter == null ? VersionFilter.MAX : versionFilter; } public void setVersionFilter(VersionFilter filter) { this.versionFilter = filter; } public boolean getIgnoreCase() { return ignoreCase; } protected void setIgnoreCase(boolean ignoreCase) { this.ignoreCase = ignoreCase; } /** * Gets the maximum rows returned by the search. * * @return the maximum rows returned by the search */ public int getMaxResultRows() { return maxResultRows == -1 ? SearchEngine.DEFAULT_MAX_ROWS : maxResultRows; } /** * Sets the maximum rows returned by the search. * * @param maxResultRows the maximum rows returned by the search */ protected void setMaxResultRows(int maxResultRows) { this.maxResultRows = maxResultRows; } /** * Sets the contentname to filter by, may be null to indicate that the filter is not set * * @param contentName the content name */ protected void setContentTypeFilter(String contentName) { this.contentType = contentName.trim().length() == 0 ? null : contentName.trim().toUpperCase(); } /** * Returns the contentname to filter by, or null if this filter option is not set. * * @return the contentname or null */ public String getContentTypeFilter() { return contentType; } /** * Returns true if the content type filter is set. * * @return true if the content type filter is set */ public boolean hasContentTypeFilter() { return contentType != null; } /** * Generates a new statement scope unique brace id. * * @return a new brace id */ protected int getNewBraceElementId() { return braceElementIdGenerator++; } /** * Add a Value to the selected elements. * * @param vi the element * @param alias the alias */ protected void addSelectedValue(final Value vi, String alias) { selected.add(new SelectedValue(vi, alias)); } /** * Returns the selected values in the correct order. * * @return the selected values */ public List<SelectedValue> getSelectedValues() { return selected; } /** * Returns the selected value matching the given alias, or null if no match can be found. * <p/> * If a alias is used more than one time the first match will be returned. * * @param alias the alias to look for * @return the matching selected value, or null */ public SelectedValue getSelectedValueByAlias(String alias) { for (SelectedValue sv : selected) { if (sv.getAlias().equalsIgnoreCase(alias)) return sv; } return null; } /** * Overrides the selected values in the given order. * * @param values the new list */ public void setSelectedValues(ArrayList<SelectedValue> values) { this.selected = values; } /** * Adds a new order by condition. * * @param vi the order by value (column) * @throws com.flexive.sqlParser.SqlParserException * if the column cannot be used for ordering because it is not selected */ public void addOrderByValue(final OrderByValue vi) throws SqlParserException { computeOrderByColumn(vi); order.add(0, vi); } /** * Returns the sort order elements. * * @return the sort order values */ public List<OrderByValue> getOrderByValues() { return order; } /** * Returns a table used by the statement by its alias. * * @param alias the alias to look for * @return a table used by the statement by its alias */ public Table getTableByAlias(String alias) { return this.tables.get(alias.toUpperCase()); } public Table getTableByType(Table.TYPE type) { for (Table tbl : getTables()) { if (tbl.getType() == type) return tbl; } return null; } /** * Returns all tables that were specified in the 'from' section of the statement. * * @return all tables */ public Table[] getTables() { Table[] result = new Table[tables.size()]; int pos = 0; for (String key : this.tables.keySet()) { result[pos++] = this.tables.get(key); } return result; } /** * Returns the root brace. * <p/> * The root brace will be null if the getType is TYPE.ALL or TYPE.EMPTY * * @return the root brace */ public Brace getRootBrace() { return this.rootBrace; } /** * Returns the statements type. * <p/> * TYPE.FILTER: there are conditions<br> * TYPE.EMPTY: the statement will not deliver any results<br> * TYPE.ALL: the statements will deliver all data from the selected sources (no filter set) * * @return the statements type */ public Type getType() { return this.type; } /** * Parses a statement. * * @param query the query to process * @return the statement * @throws SqlParserException ifthe function fails */ public static FxStatement parseSql(String query) throws SqlParserException { try { long startTime = System.currentTimeMillis(); query = cleanupQueryString(query); ByteArrayInputStream byis = new ByteArrayInputStream(query.getBytes(Charsets.UTF_8)); FxStatement stmt = new SQL(byis, "UTF-8").statement(); byis.close(); stmt.setParserExecutionTime((int) (System.currentTimeMillis() - startTime)); return stmt; } catch (TokenMgrError exc) { throw new SqlParserException(exc, query); } catch (ParseException exc) { throw new SqlParserException(exc, query); } catch (SqlParserException exc) { throw exc; } catch (Exception exc) { throw new SqlParserException(exc.getMessage()); } } protected void setParserExecutionTime(int ms) { this.parserExecutionTime = ms; } /** * Returns the execution time needed by the parser in ms. * * @return the execution time needed by the parser in ms */ public int getParserExecutionTime() { return this.parserExecutionTime; } protected void addFilter(Filter f) { this.filters.put(f.getType(), f); } /** * Returns the desired filter, or null if the filter was not specified and has no * default value. * * @param t the filter to get * @return the filter */ public Filter getFilter(Filter.TYPE t) { return filters.get(t); } protected void addTable(Table table) { if (debug) System.out.println("Adding table: " + table); this.tables.put(table.getAlias(), table); } protected boolean isTableAlias(String value) { for (String alias : this.tables.keySet()) { if (StringUtils.equalsIgnoreCase(alias, value)) { return true; } } return false; } /** * Notifies selected properties when all table aliases have been set. * parser */ private void publishTableAliases() { for (SelectedValue selectedValue : selected) { if (selectedValue.getValue() instanceof Property) { ((Property) selectedValue.getValue()).publishTableAliases(this.tables.keySet()); } } } protected Brace getCurrentBrace() { return this.currentBrace; } protected Brace startSubBrace() throws SqlParserException { Brace br = new Brace(this); currentBrace.addElement(br); currentBrace = br; return currentBrace; } protected Brace endSubBrace() throws SqlParserException { Brace parent = currentBrace.getParent(); if (currentBrace.size() == 1) { // Brace with only one element, remove it and move its element to the parent BraceElement ele = currentBrace.removeLastElement(); parent.removeElement(currentBrace); parent.addElement(ele); } currentBrace = parent; return currentBrace; } /** * Removes empty braces and handles conditions that are always false or true. * Also checks if all aliases are defined. * * @throws SqlParserException if the function fails */ protected void cleanup() throws SqlParserException { publishTableAliases(); // Check if all referenced tables are present for (SelectedValue val : selected) { if (!(val.getValue() instanceof Property)) continue; Property prop = (Property) val.getValue(); if (this.getTableByAlias(prop.getTableAlias()) == null) { String stables = ""; for (String alias : tables.keySet()) { stables += ((stables.length() > 0) ? "," : "") + alias; } throw new SqlParserException("ex.sqlSearch.filter.unknownTableAlias", prop.getTableAlias(), stables); } } // Check order by for (OrderByValue ov : getOrderByValues()) { computeOrderByColumn(ov); } // If no where clause is set at all if (this.rootBrace == null || this.rootBrace.size() == 0) { rootBrace = null; type = Type.ALL; return; } // Cleanup where clause cleanup(this.rootBrace); if (rootBrace != null) { // Nothing left and TYPE=FILTER has to be set to TYPE.EMPTY if (this.rootBrace.size() == 0 && this.getType() == Type.FILTER) { this.type = Type.EMPTY; this.rootBrace = null; } // Only one condition at top level: type has to be null and not 'or'/'and' else if (this.rootBrace.size() == 1) { this.rootBrace.setType(null); } } } public boolean isWildcardSelected() { for (SelectedValue sv : selected) { if (sv.getValue() instanceof Property) { final Property prop = (Property) sv.getValue(); if (prop.isWildcard() || prop.isUserPropsWildcard()) { return true; } } } return false; } private void computeOrderByColumn(OrderByValue ov) throws SqlParserException { if (StringUtils.isNumeric(ov.getValue())) { // column selected by index (1-based) final int index = Integer.valueOf(ov.getValue()) - 1; if (index >= 0 && selected.size() > index) { ov.setSelectedValue(index); } else if (isWildcardSelected()) { ov.setSelectedValue(0 - index); } else { throw new SqlParserException("ex.sqlSearch.invalidOrderByIndex", ov.getValue(), selected.size()); } } else { // column selected by alias boolean found = false; int pos = 0; for (SelectedValue sv : selected) { if (ov.isUsableForSorting(sv)) { found = true; ov.setSelectedValue(pos); break; } pos++; } if (!found) { throw new SqlParserException("ex.sqlSearch.invalidOrderByValue", ov.getValue()); } } } /** * Compute a cache key for the statement. * <p/> * Statements with the same cache key will produce the same resultset. * * @return the cacheKey. * @throws SqlParserException if the cache key could not be computed */ protected String getCacheKey() throws SqlParserException { try { // Only build once if (cacheKey != null) { return cacheKey; } StringBuffer key = new StringBuffer(512); if (type == Type.FILTER) { computeCacheKey(key, this.rootBrace); } else { key.append(type); } for (String table : this.tables.keySet()) { Table t = this.tables.get(table); Filter vf = t.getFilter(Filter.TYPE.VERSION); String langs = ""; for (String lang : t.getSearchLanguages()) { langs += "|" + lang; } key.append("_").append(t.getAlias()).append(";").append(t.getType()).append(";").append(langs) .append(";").append(vf == null ? "null" : vf.getValue()); } key.append("_").append("_").append(this.getMaxResultRows()).append("_") .append(this.isDistinct() ? "D" : "A"); cacheKey = key.toString(); return cacheKey; } catch (Throwable t) { System.err.println(t.getMessage()); t.printStackTrace(); throw new SqlParserException("ex.sqlSearch.unbableToBuildCachekey", t); } } /** * Sets the distinct condition of the statement. * * @param value the condition */ protected void setDistinct(boolean value) { this.distinct = value; } /** * Returns if the statements resultset is distinct. * * @return true if the statements resultset is distinct */ public boolean isDistinct() { return this.distinct; } /** * Helper function for computeCacheKey (recursion). * * @param sb the string buffer to write to * @param br the current brace */ private void computeCacheKey(StringBuffer sb, Brace br) { String brType = (br.getType() == null ? "N" : (br.getType().equalsIgnoreCase("and") ? "A" : "O")); sb.append("(").append(brType); for (BraceElement be : br.getElements()) { if (be instanceof Condition) { sb.append((" " + be.toString())); } else { computeCacheKey(sb, (Brace) be); } } sb.append(")"); } private void removeWholeBrace(Brace br, boolean alwaysTrue) { try { if (debug) System.out.println("Removing whole brace because of always " + alwaysTrue + " cond"); } catch (Exception exc) { System.err.println("###Y" + exc.getMessage()); } if (br.getParent() == null) { br.removeAllElements(); rootBrace = null; type = alwaysTrue ? Type.ALL : Type.EMPTY; } else { Brace parent = br.getParent(); try { Condition cd = new Condition(this, new Constant("1"), Condition.ValueComparator.EQUAL, new Constant(alwaysTrue ? "1" : "0")); parent.addElement(cd); } catch (Exception exc) { System.err.println("###Y" + exc.getMessage()); } parent.removeElement(br); } } /** * Perform an cleanup on the statement. * * @param br the brace to work on * @throws SqlParserException if the function fails */ private void cleanup(Brace br) throws SqlParserException { // Move down the brace tree (recursive) .. for (BraceElement be : br.getElements()) { if (be instanceof Brace) { cleanup((Brace) be); } } // ... now start cleanup from bottom up if (br.isOr()) { for (BraceElement be : br.getElements()) { if (!(be instanceof Condition)) continue; Condition cond = (Condition) be; if (cond.isAlwaysTrue()) { // Whole brace will be true, remove it and terminate loop removeWholeBrace(br, true); break; } if (cond.isAlwaysFalse()) { // Condition always false, so remove it br.removeElement(cond); } } // If the whole OR is empty we need to add an ALWAYS false to the parent if (br.size() == 0 && br.getParent() != null) { removeWholeBrace(br, false); } } else if (br.isAnd()) { for (BraceElement be : br.getElements()) { if (!(be instanceof Condition)) continue; Condition cond = (Condition) be; if (cond.isAlwaysTrue()) { // Condition always true, so remove it br.removeElement(cond); } if (cond.isAlwaysFalse()) { // Whole brace will be false, remove it and terminate loop removeWholeBrace(br, false); break; } } // If the whole AND is empty we need to add an ALWAYS true to the parent if (br.size() == 0 && br.getParent() != null) { removeWholeBrace(br, true); } } else if (br.getSize() > 0) { // Just one single condition is set, examine it! BraceElement be = br.getElementAt(0); if (be instanceof Condition) { Condition cond = (Condition) be; if (cond.isAlwaysTrue()) { // Everything is selected! br.removeAllElements(); this.rootBrace = null; this.type = Type.ALL; } else if (cond.isAlwaysFalse()) { // No result at all! br.removeAllElements(); this.rootBrace = null; this.type = Type.EMPTY; } } } // Cleanup empty braces, and braces that contain just one element if (br.size() == 1) { Brace parent = br.getParent(); if (parent != null) { BraceElement be = br.removeLastElement(); if (debug) System.out.println("Moving element to parent since its the only one left in the brace: " + be); parent.removeElement(br); parent.addElement(be); } else { // Only one brace in root brace -> make it the root brace if (br.getElementAt(0) instanceof Brace) { rootBrace = (Brace) br.removeLastElement(); } } } else if (br.size() == 0 && br.getParent() != null) { if (debug) System.out.println("Removing empty brace: " + br); br.getParent().removeElement(br); } } /** * Generates a debug string. * * @return a debug string of the statement * @throws com.flexive.shared.exceptions.FxSqlSearchException * if the function failed */ public String printDebug() throws FxSqlSearchException { try { StringBuffer result = new StringBuffer(1024); if (this.rootBrace == null) { return String.valueOf(this.getType()); } else { printDebug(result, this.rootBrace); result = result.deleteCharAt(0); } result.append(("\n##################################################\n")); int pos = 0; for (SelectedValue v : getSelectedValues()) { result.append(("Selected[" + (pos++) + "]: " + v.toString() + "\n")); } pos = 0; result.append("Order by: "); for (Value v : getOrderByValues()) { if ((pos++) > 0) result.append(","); result.append(v.toString()); } if (pos == 0) { result.append(("Order: n/a\n")); } result.append("\n"); result.append("Search Languages: "); pos = 0; for (String v : getTableByType(Table.TYPE.CONTENT).getSearchLanguages()) { if ((pos++) > 0) result.append(","); result.append(v); } result.append("\n"); result.append("Cache Key: "); try { result.append(getCacheKey()); } catch (Throwable t) { result.append(t.getMessage()); } result.append("\n"); result.append(("Parser execution time: " + this.getParserExecutionTime() + " ms\n")); return result.toString(); } catch (Throwable t) { throw new FxSqlSearchException(LOG, t, "ex.sqlSearch.printDebugFailed"); } } /** * Generates a debug string for the statement. * * @param sb the StringBuffer to write to * @param br the current bracer (recursion) */ protected void printDebug(StringBuffer sb, Brace br) { String sPrefix = "\n"; for (int i = 0; i < br.getLevel(); i++) { sPrefix += "+"; } sPrefix += (br.getType() == null ? "N" : (br.getType().equalsIgnoreCase("and") ? "A" : "O")) + " "; sb.append((sPrefix + "(")); boolean hadSubs = false; for (BraceElement be : br.getElements()) { if (be instanceof Condition) { sb.append((" " + be.toString() + " ")); } else { printDebug(sb, (Brace) be); hadSubs = true; } } if (hadSubs) { sb.append((sPrefix + ")")); } else { sb.append(" )"); } } // Private section /** * Cleanup the query string. * <p/> * This function removes comments from the query. * * @param query the query to cleanup * @return the processed query string * @throws SqlParserException ifthe function fails */ private static String cleanupQueryString(String query) throws SqlParserException { // Make sure the statement has the EOF charater at the end, also add one // '\n' to ensure that the last line comment ("--") is closed query = query.trim(); query += "\n"; if (query.charAt(query.length() - 1) != ';') query += ";"; return query; } }