Java tutorial
/*-- * Copyright 2006 Ren M. de Bloois * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package solidbase.core; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import solidbase.core.UpgradeSegment.Type; import solidbase.util.Assert; import solidstack.io.RandomAccessSourceReader; import solidstack.io.SourceLocation; /** * This class manages the upgrade file contents and the paths between versions. * * @author Ren M. de Bloois * @since Apr 1, 2006 7:18:27 PM */ public class UpgradeFile { static private final Pattern DEFINITION_MARKER_PATTERN = Pattern .compile("(SETUP|UPGRADE|SWITCH|DOWNGRADE)[ \t]+.*", Pattern.CASE_INSENSITIVE); static private final Pattern DEFINITION_PATTERN = Pattern.compile( "(SETUP|UPGRADE|SWITCH|DOWNGRADE)([ \t]+OPEN)?[ \t]+\"([^\"]*)\"[ \t]+-->[ \t]+\"([^\"]+)\"([ \t]*//.*)?", Pattern.CASE_INSENSITIVE); static private final String DEFINITION_SYNTAX_ERROR = "Line should match the following syntax: (SETUP|UPGRADE|SWITCH|DOWNGRADE) [OPEN] \"...\" --> \"...\""; static private final Pattern DEFINITION_END_PATTERN = Pattern.compile("(?:END\\s+|/)DEFINITION", Pattern.CASE_INSENSITIVE); static private final Pattern CONTROL_TABLES_PATTERN = Pattern .compile("VERSION\\s+TABLE\\s+(\\S+)\\s+LOG\\s+TABLE\\s+(\\S+)", Pattern.CASE_INSENSITIVE); static private final Pattern SEGMENT_START_MARKER_PATTERN = Pattern .compile("--\\*[ \t]*(SETUP|UPGRADE|SWITCH|DOWNGRADE).*", Pattern.CASE_INSENSITIVE); static final Pattern SEGMENT_START_PATTERN = Pattern.compile( "(SETUP|UPGRADE|SWITCH|DOWNGRADE)[ \t]+\"([^\"]*)\"[ \t]-->[ \t]+\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); static final Pattern SEGMENT_END_PATTERN = Pattern.compile("(?:END\\s+|/)(SETUP|UPGRADE|SWITCH|DOWNGRADE) *", Pattern.CASE_INSENSITIVE); // static private final Pattern INITIALIZATION_TRIGGER = Pattern.compile( "--\\*\\s*INITIALIZATION\\s*", Pattern.CASE_INSENSITIVE ); // static private final Pattern INITIALIZATION_END_PATTERN = Pattern.compile( "--\\*\\s*/INITIALIZATION\\s*", Pattern.CASE_INSENSITIVE ); // static private final Pattern INIT_CONNECTION_TRIGGER = Pattern.compile( "--\\*\\s*INIT\\s+CONNECTION.*", Pattern.CASE_INSENSITIVE ); // static private final Pattern INIT_CONNECTION_PARSER = Pattern.compile( "--\\*\\s*INIT\\s+CONNECTION(?:\\s+(\\S+)(?:\\s+USER\\s+(\\S+))?)?\\s*", Pattern.CASE_INSENSITIVE ); // static private final String INIT_CONNECTION_SYNTAX = "INIT CONNECTION <connectionname> [USER <username>]"; // static private final Pattern INIT_CONNECTION_END_PATTERN = Pattern.compile( "--\\*\\s*/INIT\\s+CONNECTION\\s*", Pattern.CASE_INSENSITIVE ); static private final String MARKER_SYNTAX_ERROR = "Line should match the following syntax: (SETUP|UPGRADE|SWITCH|DOWNGRADE) \"...\" --> \"...\"" /* or INIT CONNECTION <name> */; /** * The upgrade file. */ protected RandomAccessSourceReader file; /** * The default delimiters. */ protected Delimiter[] defaultDelimiters = SQLSource.DEFAULT_DELIMITERS; /** * All normal segments in a map indexed by source version. */ protected Map<String, Collection<UpgradeSegment>> segments = new HashMap<String, Collection<UpgradeSegment>>(); /** * Contains all known versions from the upgrade file. */ protected Set<String> versions = new HashSet<String>(); /** * All setup segments in a map indexed by source version. */ protected Map<String, UpgradeSegment> setups = new HashMap<String, UpgradeSegment>(); // /** // * Initialization fragment. // */ // protected Fragment initialization; // /** // * Positions of connection init blocks. // */ // protected List< InitConnectionFragment > connectionInits = new ArrayList< InitConnectionFragment >(); /** * The name of the version control table as defined in the upgrade file. */ protected String versionTableName; /** * The name of the log control table as defined in the upgrade file. */ protected String logTableName; /** * Constructor. * * @param file The reader which is used to read the contents of the file. */ protected UpgradeFile(RandomAccessSourceReader file) { this.file = file; } /** * Translates a segment type string to a type enum. * * @param type A segment type string. * @return The corresponding type enum. */ protected Type stringToType(String type) { if ("UPGRADE".equalsIgnoreCase(type)) return Type.UPGRADE; if ("SWITCH".equalsIgnoreCase(type)) return Type.SWITCH; if ("DOWNGRADE".equalsIgnoreCase(type)) return Type.DOWNGRADE; if ("SETUP".equalsIgnoreCase(type)) return Type.SETUP; Assert.fail("Unexpected block type '" + type + "'"); return null; } /** * Scans for segments in the file. */ protected void scan() { boolean withinDefinition = false; boolean definitionComplete = false; while (!definitionComplete) { String line = this.file.readLine(); if (line == null) throw new SourceException("Unexpected end of file", this.file.getLocation()); if (line.trim().length() > 0) { if (!line.startsWith("--*")) throw new SourceException("Line should start with --*", this.file.getLocation()); line = line.substring(3).trim(); if (line.startsWith("//")) { // ignore line } else if (line.equalsIgnoreCase("DEFINITION")) { if (withinDefinition) throw new SourceException("Unexpected DEFINITION", this.file.getLocation()); withinDefinition = true; } else if (DEFINITION_END_PATTERN.matcher(line).matches()) { if (!withinDefinition) throw new SourceException("Unexpected " + line, this.file.getLocation()); definitionComplete = true; } else if (withinDefinition) { Matcher matcher; if (DEFINITION_MARKER_PATTERN.matcher(line).matches()) { matcher = DEFINITION_PATTERN.matcher(line); if (!matcher.matches()) throw new SourceException(DEFINITION_SYNTAX_ERROR, this.file.getLocation()); String action = matcher.group(1); boolean open = matcher.group(2) != null; String source = matcher.group(3); if (source.length() == 0) source = null; String target = matcher.group(4); Type type = stringToType(action); UpgradeSegment segment = new UpgradeSegment(type, source, target, open); if (type == Type.SETUP) { if (this.setups.containsKey(source)) throw new SourceException( "Duplicate definition of init block for source version " + source, this.file.getLocation().previousLine()); this.setups.put(source, segment); } else { Collection<UpgradeSegment> segments = this.segments.get(source); if (segments == null) this.segments.put(source, segments = new LinkedList<UpgradeSegment>()); segments.add(segment); this.versions.add(source); this.versions.add(target); } } else if ((matcher = CONTROL_TABLES_PATTERN.matcher(line)).matches()) { this.versionTableName = matcher.group(1); this.logTableName = matcher.group(2); } else if ((matcher = CommandProcessor.delimiterPattern.matcher(line)).matches()) this.defaultDelimiters = CommandProcessor.parseDelimiters(matcher); else throw new SourceException("Unexpected line within definition: " + line, this.file.getLocation()); } else { if (!CommandProcessor.encodingPattern.matcher(line).matches()) throw new SourceException("Unexpected line outside definition: " + line, this.file.getLocation()); } } } String line = this.file.readLine(); while (line != null) { if (line.startsWith("--*")) { Matcher matcher; /* if( ( matcher = INITIALIZATION_TRIGGER.matcher( line ) ).matches() ) { line = this.file.readLine(); if( line == null ) throw new CommandFileException( "Premature EOF found", this.file.getLineNumber() ); int mode = 1; int pos = -1; StringBuilder builder = new StringBuilder(); while( line != null && !INITIALIZATION_END_PATTERN.matcher( line ).matches() ) { if( mode == 1 ) if( !StringUtils.isBlank( line ) ) { mode = 2; pos = this.file.getLineNumber() - 1; } if( mode == 2 ) { if( this.file.getLineNumber() > pos + 1000 ) throw new CommandFileException( "INITIALIZATION block exceeded maximum line count of 1000", pos ); builder.append( line ); builder.append( '\n' ); } line = this.file.readLine(); } if( mode == 2 ) this.initialization = new Fragment( pos, builder.toString() ); } /* else if( ( matcher = INIT_CONNECTION_TRIGGER.matcher( line ) ).matches() ) { int mode = 1; int pos = -1; StringBuilder builder = new StringBuilder(); ArrayList< InitConnectionFragment > inits = new ArrayList< InitConnectionFragment >(); while( line != null && !INIT_CONNECTION_END_PATTERN.matcher( line ).matches() ) { if( INIT_CONNECTION_TRIGGER.matcher( line ).matches() ) // Detect all markers { if( mode != 1 ) throw new CommandFileException( "INIT CONNECTION blocks can only be strictly nested", this.file.getLineNumber() - 1 ); if( !( matcher = INIT_CONNECTION_PARSER.matcher( line ) ).matches() ) throw new CommandFileException( INIT_CONNECTION_SYNTAX, this.file.getLineNumber() - 1 ); inits.add( new InitConnectionFragment( matcher.group( 1 ), matcher.group( 2 ) ) ); } else { if( mode == 1 ) if( !StringUtils.isBlank( line ) ) { mode = 2; pos = this.file.getLineNumber() - 1; } if( mode == 2 ) { if( this.file.getLineNumber() > pos + 1000 ) throw new CommandFileException( "INIT CONNECTION block exceeded maximum line count of 1000", pos ); builder.append( line ); builder.append( '\n' ); } } line = this.file.readLine(); } if( mode == 2 ) for( InitConnectionFragment initConnectionFragment : inits ) { initConnectionFragment.setText( pos, builder.toString() ); this.connectionInits.add( initConnectionFragment ); } } else */ if (SEGMENT_START_MARKER_PATTERN.matcher(line).matches()) { SourceLocation location = this.file.getLocation().previousLine(); line = line.substring(3).trim(); matcher = SEGMENT_START_PATTERN.matcher(line); if (!matcher.matches()) throw new SourceException(MARKER_SYNTAX_ERROR, location); String action = matcher.group(1); String source = matcher.group(2); String target = matcher.group(3); Type type = stringToType(action); UpgradeSegment segment; if (type == Type.SETUP) { segment = getSetupSegment(source.length() == 0 ? null : source, target); if (segment == null) throw new SourceException( "Undefined setup block found: \"" + source + "\" --> \"" + target + "\"", location); } else { segment = getSegment(source.length() == 0 ? null : source, target); if (segment == null) throw new SourceException( "Undefined upgrade block found: \"" + source + "\" --> \"" + target + "\"", location); if (segment.getType() != type) throw new SourceException( "Upgrade block type '" + action + "' is different from its definition", location); } if (segment.getLocation() != null) throw new SourceException( "Duplicate upgrade block \"" + source + "\" --> \"" + target + "\" found", location); segment.setLocation(location); } } line = this.file.readLine(); } // Check that all defined upgrade blocks are found for (Collection<UpgradeSegment> segments : this.segments.values()) for (UpgradeSegment segment : segments) if (segment.getLocation() == null) throw new FatalException("Upgrade block \"" + StringUtils.defaultString(segment.getSource()) + "\" --> \"" + segment.getTarget() + "\" not found"); // Check that all defined setup blocks are found for (UpgradeSegment segment : this.setups.values()) if (segment.getLocation() == null) throw new FatalException("Setup block \"" + StringUtils.defaultString(segment.getSource()) + "\" --> \"" + segment.getTarget() + "\" not found"); } /** * Gets the encoding of the upgrade file. * * @return The encoding of the upgrade file. */ public String getEncoding() { return this.file.getEncoding(); } /** * Close the file and all underlying streams/readers. */ protected void close() { if (this.file != null) { this.file.close(); this.file = null; } } /** * Returns the upgrade segment belonging to the specified source and target. Also checks for duplicates. * * @param source The source version. * @param target The target version. * @return The corresponding upgrade segment. */ protected UpgradeSegment getSegment(String source, String target) { UpgradeSegment result = null; Collection<UpgradeSegment> segments = this.segments.get(source); if (segments != null) for (UpgradeSegment segment : segments) if (segment.getTarget().equals(target)) { if (result != null) throw new SourceException("Duplicate upgrade block found", segment.getLocation()); result = segment; } return result; } /** * Returns the setup segment belonging to the specified source and target. Also checks for duplicates. * * @param source The source version. * @param target The target version. * @return The corresponding segment. */ protected UpgradeSegment getSetupSegment(String source, String target) { UpgradeSegment segment = this.setups.get(source); if (segment.getTarget().equals(target)) return segment; return null; } /** * Determine the best path between a source version and a target version. * * @param source The source version. * @param target The target version. * @param downgradesAllowed Allow downgrades in the resulting path. * @return The best path between a source version and a target version. This path can be empty when the source and * target are equal. The result will be null if there is no path. */ protected Path getUpgradePath(String source, String target, boolean downgradesAllowed) { Set<String> done = new HashSet<String>(); done.add(source); return getUpgradePath0(source, target, downgradesAllowed, done); } /** * Determine the best path between a source version and a target version. * * @param source The source version. * @param target The target version. * @param downgradesAllowed Allow downgrades in the resulting path. * @param targetsProcessed A set of targets that have already been processed. This prevents endless loops. * @return The best path between a source version and a target version. This path can be empty when the source and * target are equal. The result will be null if there is no path. */ protected Path getUpgradePath0(String source, String target, boolean downgradesAllowed, Set<String> targetsProcessed) { Path result = new Path(); // If equal than return an empty path if (ObjectUtils.equals(source, target)) return result; // Start with all the segments that have the given source Collection<UpgradeSegment> segments = this.segments.get(source); // As long as only one segment found loop instead of recursion while (segments != null && segments.size() == 1) { UpgradeSegment segment = segments.iterator().next(); if (targetsProcessed.contains(segment.getTarget())) // Target already processed -> no path found return null; targetsProcessed.add(segment.getTarget()); // Register target result.append(segment); // Append to result if (target.equals(segment.getTarget())) // Target is requested target -> return result return result; segments = this.segments.get(segment.getTarget()); } // No segments -> no path found if (segments == null) return null; // More then one segment found, select the best one Path selected = null; for (UpgradeSegment segment : segments) { if (targetsProcessed.contains(segment.getTarget())) // Target already processed -> ignore continue; // Build new set for recursive call Set<String> processed = new HashSet<String>(); processed.addAll(targetsProcessed); processed.add(segment.getTarget()); // Call recursive and select if better Path path = getUpgradePath0(segment.getTarget(), target, downgradesAllowed, processed); if (path != null) { path.prepend(segment); if (selected == null) selected = path; else if (path.betterThan(selected)) selected = path; } } // No segments found -> no path found if (selected == null) return null; // Return the selected path appended to the first path return result.append(selected); } /** * Returns a setup path for the specified source. As setup is always done to the latest version, no target is needed. * * @param source The source version. * @return A list of setup segments that correspond to the given source. */ protected List<UpgradeSegment> getSetupPath(String source) { List<UpgradeSegment> result = new ArrayList<UpgradeSegment>(); // Branches not possible // Start with all the segments that start with the given source UpgradeSegment segment = this.setups.get(source); while (segment != null) { result.add(segment); segment = this.setups.get(segment.getTarget()); } if (result.size() > 0) return result; return null; } /** * Determines all possible target versions from the specified source version. The current version is also considered. * * @param version Current version. * @param targeting Indicates that we are already targeting a specific version. * @param tips Only return tip versions. * @param downgradesAllowed Allow downgrades. * @param prefix Only consider versions that start with the given prefix. * @param result All results are added to this set. */ protected void collectTargets(String version, String targeting, boolean tips, boolean downgradesAllowed, String prefix, Set<String> result) { Assert.notNull(result, "'result' must not be null"); // Will throw errors when the version or targeting version is not available in the upgrade file collectReachableVersions(version, targeting, downgradesAllowed, result); // Filter out all version that do not start with the prefix if (prefix != null) for (Iterator<String> iterator = result.iterator(); iterator.hasNext();) { String s = iterator.next(); if (s == null || !s.startsWith(prefix)) iterator.remove(); } // Filter out all versions that are not the tips of the reachable path if (tips) for (Iterator<String> iterator = result.iterator(); iterator.hasNext();) { String v = iterator.next(); Collection<UpgradeSegment> segments = this.segments.get(v); if (segments != null) for (UpgradeSegment segment : segments) if (segment.isUpgrade()) if (prefix == null || segment.getTarget().startsWith(prefix)) { iterator.remove(); break; } } } /** * Gets all versions that are reachable from the given source version. * * @param source The source version. * @param targeting Indicates that we are already targeting a specific version. * @param downgradesAllowed Allow downgrades. * @return Returns an ordered set of versions that are reachable from the given source version. */ protected LinkedHashSet<String> getReachableVersions(String source, String targeting, boolean downgradesAllowed) { LinkedHashSet<String> result = new LinkedHashSet<String>(); collectReachableVersions(source, targeting, downgradesAllowed, result); return result; } /** * Retrieves all versions that are reachable from the given source version. The current version is also considered. * * @param source The source version. * @param targeting Already targeting a specific version. * @param downgradesAllowed Allow downgrades. * @param result This set gets filled with all versions that are reachable from the given source version. */ protected void collectReachableVersions(String source, String targeting, boolean downgradesAllowed, Set<String> result) { if (!this.versions.contains(source)) throw new FatalException("The current database version " + StringUtils.defaultString(source, "<no version>") + " is not available in the upgrade file. Maybe this version is deprecated or the wrong upgrade file is used."); if (targeting == null) result.add(source); // The source is reachable Collection<UpgradeSegment> segments = this.segments.get(source); // Get all segments with the given source if (segments == null) return; // Queue contains segments that await processing LinkedList<UpgradeSegment> queue = new LinkedList<UpgradeSegment>(); // Fill queue with segments if (targeting != null) { for (UpgradeSegment segment : segments) if (targeting.equals(segment.getTarget())) queue.add(segment); // Add segment to the end of the list if (queue.isEmpty()) throw new FatalException("The database is incompletely upgraded to version " + targeting + ", but that version is not reachable from version " + StringUtils.defaultString(source, "<no version>")); } else queue.addAll(segments); // Process the queue while (!queue.isEmpty()) { UpgradeSegment segment = queue.removeFirst(); // pop() is not available in java 5 if (!result.contains(segment.getTarget())) // Already there? if (downgradesAllowed || !segment.isDowngrade()) // Downgrades allowed? { result.add(segment.getTarget()); if (!segment.isOpen()) // Stop when segment is open. { segments = this.segments.get(segment.getTarget()); // Add the next to the queue if (segments != null) queue.addAll(segments); // Add segments to the end of the list } } } } /** * Jump to the position in the upgrade file where the given segment starts. * * @param segment The upgrade segment to jump to. * @return The source for the given segment. */ protected UpgradeSource gotoSegment(UpgradeSegment segment) { Assert.isTrue(segment.getLocation() != null, "Upgrade or setup block not found"); this.file.gotoLine(segment.getLineNumber()); String line = this.file.readLine(); // System.out.println( line ); Assert.isTrue(SEGMENT_START_MARKER_PATTERN.matcher(line).matches()); UpgradeSource source = new UpgradeSource(this.file); source.setDelimiters(this.defaultDelimiters); return source; } }