/* * Copyright (c) 2004-2013 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 * Purdue University */ package org.postgresql.pljava.sqlgen; import java.io.IOException; import java.io.Writer; import java.util.regex.Matcher; import java.util.regex.Pattern; import static javax.tools.Diagnostic.Kind.ERROR; import static javax.tools.StandardLocation.CLASS_OUTPUT; import static org.postgresql.pljava.sqlgen.Lexicals.ISO_PG_JAVA_IDENTIFIER; /** * Class for writing an SQLJ deployment descriptor file in proper form, given * snippets of SQL code for deployment and undeployment that have already been * arranged into a workable order. * * @author Thomas Hallgren - pre-Java6 version * @author Chapman Flack (Purdue Mathematics) - update to Java6, reorganize, * add lexable check */ public class DDRWriter { /** * Generate the deployment descriptor file. * * @param snips Code snippets to include in the file, in a workable order * for the install actions group. The remove actions group will be generated * by processing this array in reverse order. * @param p Reference to the calling object, used to obtain the Filer * object and desired output file name, and for diagnostic messages. */ static void emit( Snippet[] snips, DDRProcessorImpl p) throws IOException { if ( ! ensureLexable( snips, p) ) return; Writer w = p.filr.createResource( CLASS_OUTPUT, "", p.output).openWriter(); w.write( "SQLActions[]={\n\"BEGIN INSTALL\n"); for ( Snippet snip : snips ) for ( String s : snip.deployStrings() ) writeCommand( w, s, snip.implementor()); w.write( "END INSTALL\",\n\"BEGIN REMOVE\n"); for ( int i = snips.length; i --> 0; ) for ( String s : snips[i].undeployStrings() ) writeCommand( w, s, snips[i].implementor()); w.write( "END REMOVE\"\n}\n"); w.close(); } /** * Write a single command into the current (install or remove) action group. * Can emit either the implementor-specific or non-specific command form. * * @param w The Writer object on which the command should be written * @param s The command to write. If implementor is null, the command will * be written as is, with only a semicolon added. If implementor is not * null, then s will be wrapped in a BEGIN implementor s END implementor; * construct. * @param implementor The case-insensitive string identifying specific * SQL implementation targeted by the command, or null if the command * is implementor-nonspecific. PostgreSQL is the string to use for * PostgreSQL-specific commands. */ static void writeCommand( Writer w, String s, String implementor) throws IOException { if ( null != implementor ) { w.write( "BEGIN "); w.write( implementor); w.write( '\n'); } w.write( s); if ( null != implementor ) { w.write( "\nEND "); w.write( implementor); } w.write( ";\n"); } /** * Check that the snippets to be written into the deployment descriptor * file will survive the code that reads them in. The code in * SQLDeploymentDescriptor.java makes no attempt to recognize SQL, but * uses simple rules about backslash escapes and balanced ' and " quote * pairs to read in snippets intact and find the terminating ; (and not, * for example, a quoted ; somewhere in the snippet). Happily, those rules * can actually work: the SQL convention of escaping a ' or " by doubling * it plays nicely with the simple balanced-quotes rule, and backslashes * can be made to work as the rule assumes (e.g. as long as they are inside * e'' strings or standard_conforming_strings is off). * * That means it is at least possible to create annotations that survive * being written to the .ddr file and read in again, but not necessarily * without thinking. When the snippets pass this check, that means they'll * be succesfully recovered by SQLDeploymentDescriptor.java and passed along * in that form to the backend; it does not guarantee the backend will make * the intended sense of them. For that to happen, developers must be * careful to write the strings in annotations in ways that both satisfy * these rules and make sense to the backend. For example, it is best to use * the e'' form for string constants, because the backend's interpretation * of the '' form may or may not match these rules depending on the setting * of standard_conforming_strings. Dollar-quoting is a great idea for some * jobs but not for this one, because the deployment descriptor lexer * doesn't grok it and will be foiled by any literal \ ' " in the content. * * Fortunately, generating a deployment descriptor is not the sort of task * where the risk model includes deliberate injection attacks by an * adversary, but just a developer who presumably wants his or her code to * work right, and might simply have to be extra careful and need more than * one try before it does. * * @param snips Array of snippets whose deployStrings and undeployStrings * will be checked for unbalanced quoting * @param p Reference to the DDRProcessorImpl to be used for error reporting * @return true if the snippets satisfy the rules to be succesfully read * back from the .ddr file. */ static boolean ensureLexable( Snippet[] snips, DDRProcessorImpl p) { boolean errorRaised = false; Matcher m = checker.matcher( ""); /* * Restricting this identifier to satisfy Java rules as well as SQL ones * is unnecessarily restrictive (it isn't going to appear in Java code, * after all), but at present those are the rules used to scan it in * SQLDeploymentDescriptor, so it had better fit. */ Matcher i = ISO_PG_JAVA_IDENTIFIER.matcher( ""); for ( Snippet snip : snips ) { String implementor = snip.implementor(); if ( null != implementor ) { i.reset( implementor); if ( ! i.matches() ) { p.msg( ERROR, "non-SQL- or -Java-structured implementor-name: %s", implementor ); } } for ( String s : snip.deployStrings() ) { m.reset( s); if ( ! m.matches() ) { p.msg( ERROR, checkMsg, "install", s); errorRaised = true; } } for ( String s : snip.undeployStrings() ) { m.reset( s); if ( ! m.matches() ) { p.msg( ERROR, checkMsg, "remove", s); errorRaised = true; } } } return ! errorRaised; } /** * Return the e'...' form of quoting a string (the only one compatible with * the lexer rules for re-reading the .ddr file, without needing to know * how standard_conforming_strings is set on the backend). * * @param s Something to be quoted * @return The e-quoted form of s */ public static String eQuote( CharSequence s) { Matcher m = equoter.matcher( s); return "e'" + m.replaceAll( "$0$0") + '\''; } static final Pattern checker = Pattern.compile( "(?s:\\\\.|[^;'\"]|'(?:\\\\.|[^'])*+'|\"(?:\\\\.|[^\"])*+\")*+"); static final String checkMsg = "%s command contains unquoted ; or unbalanced '/\": %s"; static final Pattern equoter = Pattern.compile( "\\\\|'"); }