All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.maven.sql.MigrationHints Maven / Gradle / Ivy

There is a newer version: 21.16.1.0
Show newest version
/*
 * Tentackle - http://www.tentackle.org.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.tentackle.maven.sql;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringTokenizer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.shared.model.fileset.FileSet;
import org.apache.maven.shared.model.fileset.util.FileSetManager;
import org.tentackle.common.Settings;
import org.tentackle.model.migrate.ColumnMigration;

/**
 * Migration hints.
 * 

* * The hint files are of the following format: * *

 * <keyword>: line
 * line
 * line
 * ...
 *
 * <keyword>:
 * 
* * Kewords start at the first character of a line and are immediately followed by a colon. Their scope spans to the end of the current and * all following lines until the next keyword. *
* Valid keywords are: * *
    *
  • before all: SQL code to be executed before everything else
  • *
  • after all: SQL code to be executed after everything else
  • *
  • before <tablename>: SQL code to be executed before the migration of given table
  • *
  • after <tablename>: SQL code to be executed after the migration of given table
  • *
  • migrate <tablename>: explicit SQL code to migrate given table
  • *
  • migrate <tablename>#<columnname>[/<newcolumnname>]: explicit SQL code to migrate given column
  • *
  • hint <tablename>: migration hint (regular expression). Used to select automatic migrations that were commented out * as alternatives by the table migrator.
  • *
  • depend <tablename>: tablename, tablename,... wait until all given tablenames are migrated *
* * Several hints for the same table/section will be concatenated. * * @author harald */ public class MigrationHints { // keywords private static final String BEFORE_ALL = "before all"; private static final String AFTER_ALL = "after all"; private static final String BEFORE = "before"; private static final String AFTER = "after"; private static final String MIGRATE = "migrate"; private static final String HINT = "hint"; private static final String DEPEND = "depend"; /** * Key for explicit column migration. */ private static class ColumnKey { private final String columnName; // the (old) columnname private final String newColumnName; // the new columnname (if renamed) private ColumnKey(String columnName, String newColumnName) { this.columnName = columnName; this.newColumnName = newColumnName; } @Override public int hashCode() { int hash = 7; hash = 23 * hash + Objects.hashCode(this.columnName); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ColumnKey other = (ColumnKey) obj; return Objects.equals(this.columnName, other.columnName); } } private final StringBuilder beforeAll; // SQL to execute at the very beginning, null if none private final StringBuilder afterAll; // SQL to execute at the very end, null if none private final Map beforeTable; // SQL to execute before automatic table migration private final Map afterTable; // SQL to execute after automatic table migration private final Map migrateTable; // SQL to execute instead automatic table migration private final Map> migrateColumns; // SQL to execute instead automatic table migration > private final Map> dependencies; // dependencies > private final Map> hints; // hints for the automatic table migrators /** * parsing section. */ private class Section { private final String keyword; // the keyword private String tableName; // the tablename, null if none private String oldColumnName; // optional old column name private String newColumnName; // optional new column name private StringBuilder text; // the content /** * Creates a section. * * @param keyword the keyword */ private Section(String keyword) { this.keyword = keyword; } /** * Finishes a parsed section. */ private void finish() throws MojoExecutionException { switch(keyword) { case BEFORE_ALL: if(beforeAll.length() > 0) { beforeAll.append('\n'); } beforeAll.append(text); break; case AFTER_ALL: if (afterAll.length() > 0) { afterAll.append('\n'); } afterAll.append(text); break; case BEFORE: String before = beforeTable.get(tableName); if (before != null) { text.insert(0, '\n'); text.insert(0, before); } beforeTable.put(tableName, text.toString()); break; case AFTER: String after = afterTable.get(tableName); if (after != null) { text.insert(0, '\n'); text.insert(0, after); } afterTable.put(tableName, text.toString()); break; case MIGRATE: if (oldColumnName != null) { // column migration ColumnKey colKey = new ColumnKey(oldColumnName, newColumnName); Map columns = migrateColumns.computeIfAbsent(tableName, k -> new HashMap<>()); String migrate = columns.get(colKey); if (migrate != null) { text.insert(0, '\n'); text.insert(0, migrate); } columns.put(colKey, text.toString()); } else { // table migration String migrate = migrateTable.get(tableName); if (migrate != null) { text.insert(0, '\n'); text.insert(0, migrate); } migrateTable.put(tableName, text.toString()); } break; case HINT: try { Pattern pattern = Pattern.compile(text.toString(), Pattern.DOTALL); Collection patterns = hints.computeIfAbsent(tableName, k -> new ArrayList<>()); patterns.add(pattern); } catch (PatternSyntaxException pex) { throw new MojoExecutionException("migration hint malformed", pex); } break; case DEPEND: Collection tables = dependencies.computeIfAbsent(tableName, k -> new ArrayList<>()); StringTokenizer stok = new StringTokenizer(text.toString(), ", \t\r\n"); while (stok.hasMoreTokens()) { tables.add(stok.nextToken().trim().toLowerCase()); } break; } } } /** * Creates the migration hints for a backend. */ public MigrationHints() { beforeAll = new StringBuilder(); afterAll = new StringBuilder(); beforeTable = new HashMap<>(); afterTable = new HashMap<>(); migrateTable = new HashMap<>(); migrateColumns = new HashMap<>(); hints = new HashMap<>(); dependencies = new HashMap<>(); } /** * Gets the sql code to be executed before anything else. * * @return the sql code, never null */ public String getBeforeAll() { return beforeAll.toString(); } /** * Gets the sql code to be executed after anything else. * * @return the sql code, never null */ public String getAfterAll() { return afterAll.toString(); } /** * Gets the sql code to be executed before table migration. * * @param tableName the tablename * @return the sql code, null if no such sql */ public String getBeforeTable(String tableName) { return beforeTable.get(tableName); } /** * Gets the sql code to be executed after table migration. * * @param tableName the tablename * @return the sql code, null if no such sql */ public String getAfterTable(String tableName) { return afterTable.get(tableName); } /** * Gets the sql code to be executed instead of table migration. * * @param tableName the tablename * @return the sql code, null if use automatic migration */ public String getMigrateTable(String tableName) { return migrateTable.get(tableName); } /** * Gets the migration hints. * * @param tableName the tablename * @return the hint patterns, null if no hints for this table */ public Collection getHints(String tableName) { return hints.get(tableName); } /** * Gets the explicit column migrations for a table. * * @param tableName the table * @return the explicit column migrations, null if none */ public Collection getColumnMigrations(String tableName) { Map columns = migrateColumns.get(tableName); if (columns != null) { Collection migrations = new ArrayList<>(); for (Map.Entry entry: columns.entrySet()) { migrations.add(new ColumnMigration(entry.getKey().columnName, entry.getKey().newColumnName, entry.getValue())); } return migrations; } return null; } /** * Gets the dependencies. * * @param tableName the tablename * @return the dependencies, null if no dependencies for this table */ public Collection getDependencies(String tableName) { return dependencies.get(tableName); } /** * Load the migration hints. * * @param filesets the filesets to load hints from * @param filter file filter, null if none * @param logger the maven logger * @param verbose true if verbose * @param resourceDirs resource dirs, null if none * @throws MojoExecutionException if failed */ public void load(List filesets, Predicate filter, Log logger, boolean verbose, List resourceDirs) throws MojoExecutionException { if (filesets != null) { for (FileSet fileset: filesets) { FileSetManager fileSetManager = new FileSetManager(logger, verbose); List hintDirNames; if (fileset.getDirectory() == null) { hintDirNames = resourceDirs; // try all resource directories if (hintDirNames == null || hintDirNames.isEmpty()) { throw new MojoExecutionException("no given in of and no resource directories found"); } } else { hintDirNames = new ArrayList<>(); hintDirNames.add(fileset.getDirectory()); // use given directory } for (String propertiesDirName: hintDirNames) { fileset.setDirectory(propertiesDirName); String[] fileNames = fileSetManager.getIncludedFiles(fileset); if (fileNames.length > 0) { for (String filename : fileNames) { File hintFile = new File(new File(propertiesDirName), filename); if (filter == null || filter.test(hintFile)) { logger.debug("loading migration hints from " + hintFile); StringBuilder buf = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream(hintFile), Settings.encodingCharset))) { char[] readBuf = new char[1024]; int len; while ((len = reader.read(readBuf)) != -1) { buf.append(readBuf, 0, len); } } catch (IOException ex) { throw new MojoExecutionException("reading migration hints from '" + hintFile + "' failed", ex); } String delims = "\n\r"; StringTokenizer lineTokenizer = new StringTokenizer(buf.toString(), delims, true); int lineNumber = 1; Section section = null; // current section try { while (lineTokenizer.hasMoreTokens()) { String line = lineTokenizer.nextToken(); if (line.length() == 1) { if (delims.contains(line)) { // delimiter if (line.equals("\n")) { lineNumber++; } continue; } } Section newSection = nextSection(line); if (newSection != null) { if (section != null) { section.finish(); } section = newSection; } else if (section != null) { section.text.append('\n'); section.text.append(line); } } if (section != null) { section.finish(); } } catch (MojoExecutionException mox) { String msg = mox.getMessage() + " in " + hintFile + ", line " + lineNumber; if (mox.getCause() != null) { throw new MojoExecutionException(msg); } throw new MojoExecutionException(msg, mox.getCause()); } } else { logger.debug(hintFile + " skipped"); } } } } } } } /** * Returns the new section if line starts a new one. * * @param line the line * @return the section, null if line does not start with a keyword */ private Section nextSection(String line) throws MojoExecutionException { Section section = null; if (line.startsWith(BEFORE_ALL)) { section = new Section(BEFORE_ALL); } else if (line.startsWith(AFTER_ALL)) { section = new Section(AFTER_ALL); } else if (line.startsWith(BEFORE)) { section = new Section(BEFORE); } else if (line.startsWith(AFTER)) { section = new Section(AFTER); } else if (line.startsWith(MIGRATE)) { section = new Section(MIGRATE); } else if (line.startsWith(HINT)) { section = new Section(HINT); } else if (line.startsWith(DEPEND)) { section = new Section(DEPEND); } if (section != null) { // read optional tablename int colonNdx = line.indexOf(':'); if (colonNdx == -1) { section = null; // no valid section } else { section.tableName = line.substring(section.keyword.length(), colonNdx).trim().toLowerCase(); int dotNdx = section.tableName.indexOf('#'); if (dotNdx > 0) { section.oldColumnName = section.tableName.substring(dotNdx + 1); section.tableName = section.tableName.substring(0, dotNdx); int slashNdx = section.oldColumnName.indexOf('/'); if (slashNdx > 0) { section.newColumnName = section.oldColumnName.substring(slashNdx + 1); section.oldColumnName = section.oldColumnName.substring(0, slashNdx); } } section.text = new StringBuilder(line.substring(colonNdx + 1).trim()); // validate keyword switch (section.keyword) { case BEFORE: case AFTER: case MIGRATE: case HINT: case DEPEND: if (section.tableName.isEmpty()) { throw new MojoExecutionException("missing tablename"); } if (!isValidTableName(section.tableName)) { section = null; } break; case BEFORE_ALL: case AFTER_ALL: if (!section.tableName.isEmpty()) { section = null; // colon not immediately following the keyword } } } } return section; } /** * Checks if the given string is a valid tablename. * * @param str the string * @return true if could be a tablename */ private boolean isValidTableName(String str) { boolean lastWasDot = true; int dotCount = 0; if (str != null && !str.isEmpty()) { for (int pos = 0; pos < str.length(); pos++) { char c = str.charAt(pos); if (c == '.') { lastWasDot = true; dotCount++; if (dotCount > 1) { return false; // only one dot is allowed to separate schema from tablename } } else { if (lastWasDot) { lastWasDot = false; if (Character.isDigit(c)) { return false; // must not start with a digit } } else { if (!Character.isDigit(c) && // digit is ok if not at start of table- or schema name !Character.isAlphabetic(c) && c != '_') { // must be digit, alphabetic or underscore return false; } } } } return true; } else { return false; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy