/* * Copyright (c) 2004-2016 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License * which accompanies this distribution, and is available at * http://opensource.org/licenses/BSD-3-Clause * * Contributors: * Tada AB * Chapman Flack */ package org.postgresql.pljava.management; import java.sql.Connection; import java.sql.SQLException; import java.text.ParseException; import java.util.ArrayList; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.postgresql.pljava.sqlgen.Lexicals.Identifier; import static org.postgresql.pljava.sqlgen.Lexicals.identifierFrom; import static org.postgresql.pljava.sqlgen.Lexicals.ISO_AND_PG_IDENTIFIER_CAPTURING; /** * This class deals with parsing and executing the deployment descriptor as * defined in ISO/IEC 9075-13:2003. It has the following format:<pre><code> * <descriptor file> ::= * SQLActions <left bracket> <right bracket> <equal sign> * { [ <double quote> <action group> <double quote> * [ <comma> <double quote> <action group> <double quote> ] ] } * * <action group> ::= * <install actions> * | <remove actions> * * <install actions> ::= * BEGIN INSTALL [ <command> <semicolon> ]... END INSTALL * * <remove actions> ::= * BEGIN REMOVE [ <command> <semicolon> ]... END REMOVE * * <command> ::= * <SQL statement> * | <implementor block> * * <SQL statement> ::= <SQL token>... * * <implementor block> ::= * BEGIN <implementor name> <SQL token>... END <implementor name> * * <implementor name> ::= <identifier> * * <SQL token> ::= an SQL lexical unit specified by the term "<token>" in * Subclause 5.2, "<token>" and "<separator>", in ISO/IEC 9075-2.</code></pre> * * <p><strong>Note:</strong> this parser departs from the specification for * {@code <descriptor file>} in the following ways:</p> * <ul> * <li>Per ISO/IEC 9075-13, an {@code <SQL statement>} (not wrapped as an * {@code <implementor block>}), may be one of only a few types of statement: * declaring a procedure, function, or type; granting {@code USAGE} on a * type, or {@code EXECUTE} on a procedure or function; declaring the ordering * of a type. Any SQL that is not one of those, or does not use the exact * ISO/IEC 9075 syntax, is allowed only within an {@code <implementor block>}. * This parser does not enforce that restriction. This behavior is strictly * more lax than the spec, and will not reject any standards-conformant * descriptor file.</li> * <li>Officially, an {@code <implementor name>} is an SQL {@code identifier} * and may have any of the forms defined in 9075-2 subclause 5.2. This parser * (a) only recognizes the {@code <regular identifier>} form (that is, * non-double-quoted, no Unicode escape, matched case-insensitively), and (b) * replaces the SQL allowable-character rules {@code <identifier start>} and * {@code <identifier extend>} with the similar but nonidentical Java rules * {@link Character#isJavaIdentifierStart(char)} and * {@link Character#isJavaIdentifierPart(char)} (which do not work for * characters in the {@linkplain Character#isSupplementaryCodePoint(int) * supplementary character} range). In unlikely cases this * could lead to rejecting a deployment descriptor that in fact conforms to the * standard.</li> * <li>Through PL/Java 1.4.3, this parser has not recognized {@code --} as the * start of an SQL comment (which it is), and <em>has</em> recognized * {@code //}, which isn't.</li> * <li>Also through PL/Java 1.4.3, all whitespace (outside of quoted literals * and identifiers) has been collapsed to a single {@code SPACE}, which would * run afoul of the SQL rules for quoted literal/identifier continuation, if * a deployment descriptor were ever to use that.</li> * </ul> * <p>The most conservative way to generate a deployment descriptor for * PL/Java's consumption is to wrap <em>all</em> commands as * {@code <implementor block>} and ensure that any {@code <implementor name>} * is both a valid Java identifier and a valid SQL {@code <regular identifier>} * containing nothing from the supplementary character range. * </p> * @author Thomas Hallgren * @author Chapman Flack */ public class SQLDeploymentDescriptor { private final ArrayList<Command> m_installCommands = new ArrayList<>(); private final ArrayList<Command> m_removeCommands = new ArrayList<>(); private final StringBuffer m_buffer = new StringBuffer(); private final char[] m_image; private final Logger m_logger; private int m_position = 0; private static final Pattern s_beginImpl = Pattern.compile(String.format( "^(?i:BEGIN)\\s++(?:%1$s)\\s*+", ISO_AND_PG_IDENTIFIER_CAPTURING)); private static final Pattern s_endImpl = Pattern.compile(String.format( "(?<!\\w)(?i:END)\\s++(?:%1$s)$", ISO_AND_PG_IDENTIFIER_CAPTURING)); /** * Parses the deployment descriptor <code>descImage</code> into a series of * {@code Command} objects each having an SQL command and, if present, an * {@code <implementor name>}. The install and remove blocks are remembered * for later execution * with calls to {@link #install install()} and {@link #remove remove()}. * @param descImage The image to parse * @throws ParseException If a parse error is encountered */ public SQLDeploymentDescriptor(String descImage) throws ParseException { m_image = descImage.toCharArray(); m_logger = Logger.getAnonymousLogger(); this.readDescriptor(); } /** * Executes the <code>INSTALL</code> actions. * @param conn The connection to use for the execution. * @throws SQLException */ public void install(Connection conn) throws SQLException { this.executeArray(m_installCommands, conn); } /** * Executes the <code>REMOVE</code> actions. * @param conn The connection to use for the execution. * @throws SQLException */ public void remove(Connection conn) throws SQLException { this.executeArray(m_removeCommands, conn); } /** * Returns the original image. */ public String toString() { return new String(m_image); } private void executeArray(ArrayList<Command> array, Connection conn) throws SQLException { m_logger.entering("org.postgresql.pljava.management.SQLDeploymentDescriptor", "executeArray"); for( Command c : array ) c.execute( conn); m_logger.exiting("org.postgresql.pljava.management.SQLDeploymentDescriptor", "executeArray"); } private ParseException parseError(String msg) { return new ParseException(msg, m_position); } private void readDescriptor() throws ParseException { m_logger.entering("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readDescriptor"); if(!"SQLACTIONS".equals(this.readIdentifier())) throw this.parseError("Expected keyword 'SQLActions'"); this.readToken('['); this.readToken(']'); this.readToken('='); this.readToken('{'); for(;;) { readActionGroup(); if(readToken("},") == '}') { // Only whitespace allowed now // int c = this.skipWhite(); if(c >= 0) throw this.parseError( "Extraneous characters at end of descriptor"); m_logger.exiting("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readDescriptor"); return; } } } private void readActionGroup() throws ParseException { m_logger.entering("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readActionGroup"); this.readToken('"'); if(!"BEGIN".equals(this.readIdentifier())) throw this.parseError("Expected keyword 'BEGIN'"); ArrayList<Command> commands; String actionType = this.readIdentifier(); if("INSTALL".equals(actionType)) commands = m_installCommands; else if("REMOVE".equals(actionType)) commands = m_removeCommands; else throw this.parseError("Expected keyword 'INSTALL' or 'REMOVE'"); for(;;) { String cmd = this.readCommand(); // Check if the cmd is in the form: // // <implementor block> ::= // BEGIN <implementor name> <SQL token>... END <implementor name> // // If it is, keep track of the <implementor name> with the cmd. // Identifier implementorName = null; if(cmd.length() >= 15) { Matcher m = s_beginImpl.matcher(cmd); if ( m.find() ) { Identifier begIdent = identifierFrom(m); int pos = m.end(); m = s_endImpl.matcher(cmd); if ( ! m.find(pos) ) throw this.parseError( "BEGIN <implementor name> without matching END"); Identifier endIdent = identifierFrom(m); if ( ! endIdent.equals(begIdent) ) throw this.parseError(String.format( "BEGIN \"%1$s\" and END \"%2$s\" do not match", begIdent, endIdent)); implementorName = begIdent; cmd = cmd.substring(pos, m.start()); } } commands.add(new Command(cmd.trim(), implementorName)); // Check if we have END INSTALL or END REMOVE // int savePos = m_position; try { String tmp = this.readIdentifier(); if("END".equals(tmp)) { tmp = this.readIdentifier(); if(actionType.equals(tmp)) break; } m_position = savePos; } catch(ParseException e) { m_position = savePos; } } this.readToken('"'); m_logger.exiting("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readActionGroup"); } private String readCommand() throws ParseException { m_logger.entering("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readCommand"); int startQuotePos = -1; int inQuote = 0; int c = this.skipWhite(); m_buffer.setLength(0); while(c != -1) { switch(c) { case '\\': m_buffer.append((char)c); c = this.read(); if(c != -1) { m_buffer.append((char)c); c = this.read(); } break; case '"': case '\'': if(inQuote == 0) { startQuotePos = m_position; inQuote = c; } else if(inQuote == c) { startQuotePos = -1; inQuote = 0; } m_buffer.append((char)c); c = this.read(); break; case ';': if(inQuote == 0) { String cmd = m_buffer.toString(); m_logger.exiting("org.postgresql.pljava.management.SQLDeploymentDescriptor", "readCommand", cmd); return cmd; } m_buffer.append((char)c); c = this.read(); break; default: if(inQuote == 0 && Character.isWhitespace((char)c)) { // Change multiple whitespace into one singe space. // m_buffer.append(' '); c = this.skipWhite(); } else { m_buffer.append((char)c); c = this.read(); } } } if(inQuote != 0) throw this.parseError("Untermintated " + (char)inQuote + " starting at position " + startQuotePos); throw this.parseError("Unexpected EOF. Expecting ';' to end command"); } private int skipWhite() throws ParseException { int c; for(;;) { c = this.read(); if(c >= 0 && Character.isWhitespace((char)c)) continue; if(c == '/') { switch(this.peek()) { // "//" starts a line comment. Skip until end of line. // case '/': this.skip(); for(;;) { c = this.read(); switch(c) { case '\n': case '\r': case -1: break; default: continue; } break; } continue; // "/*" starts a line comment. Skip until "*/" // case '*': this.skip(); for(;;) { c = this.read(); switch(c) { case -1: throw this.parseError( "Unexpected EOF when expecting end of multi line comment"); case '*': if(this.peek() == '/') { this.skip(); break; } continue; default: continue; } break; } continue; } } break; } return c; } private String readIdentifier() throws ParseException { int c = this.skipWhite(); if(c < 0) throw this.parseError("Unexpected EOF when expecting start of identifier"); char ch = (char)c; if(!Character.isJavaIdentifierStart(ch)) throw this.parseError( "Syntax error at '" + ch + "', expected identifier"); m_buffer.setLength(0); m_buffer.append(ch); for(;;) { c = this.peek(); if(c < 0) break; ch = (char)c; if(Character.isJavaIdentifierPart(ch)) { m_buffer.append(ch); this.skip(); continue; } break; } return m_buffer.toString().toUpperCase(); } private char readToken(String tokens) throws ParseException { int c = this.skipWhite(); if(c < 0) throw this.parseError("Unexpected EOF when expecting one of \"" + tokens + '"'); char ch = (char)c; if(tokens.indexOf(ch) < 0) throw this.parseError( "Syntax error at '" + ch + "', expected one of '" + tokens + "'"); return ch; } private char readToken(char token) throws ParseException { int c = this.skipWhite(); if(c < 0) throw this.parseError("Unexpected EOF when expecting token '" + token + '\''); char ch = (char)c; if(ch != token) throw this.parseError( "Syntax error at '" + ch + "', expected '" + token + "'"); return ch; } private int peek() { return (m_position >= m_image.length) ? -1 : m_image[m_position]; } private void skip() { m_position++; } private int read() { int pos = m_position++; return (pos >= m_image.length) ? -1 : m_image[pos]; } } /** * A {@code <command>} in the deployment descriptor grammar. * If {@link #tag} is {@code null}, this is an {@code <SQL statement>} * (that is, to be run unconditionally, though no attempt has been made to * restrict it to the five types of standard-conforming statements allowed * by the spec). If {@code tag} is not null, this is an * {@code <implementor block>}, to be executed conditionally based on the * value of {@code tag}. * <p> * It could seem tempting to subclass this and assign specific behaviors, but * really it is created too early for that. At the time of creation only the * tag name (or absence) is known. Which tags are to be honored with what sort * of behavior will not be known for all tags, and may change as commands are * executed. */ class Command { /** The sql to execute (if this command is not suppressed). Never null. */ final String sql; private final Identifier tag; /** * Execute this {@code Command} using a {@code DDRExecutor} chosen * according to its {@code <implementor name>}. */ void execute( Connection conn) throws SQLException { DDRExecutor ddre = DDRExecutor.forImplementor( tag); ddre.execute( sql, conn); } Command(String sql, Identifier tag) { this.sql = sql.trim(); this.tag = tag; } public String toString() { if ( null == tag ) return "/*<SQL statement>*/ " + sql; return "/*<implementor block> " + tag + "*/ " + sql; } }