001package io.ebean.migration;
002
003import java.util.Arrays;
004
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008/**
009 * The version of a migration used so that migrations are processed in order.
010 */
011public class MigrationVersion implements Comparable<MigrationVersion> {
012
013  private static final Logger logger = LoggerFactory.getLogger(MigrationVersion.class);
014
015  private static final int[] REPEAT_ORDERING_MIN = {Integer.MIN_VALUE};
016
017  private static final int[] REPEAT_ORDERING_MAX = {Integer.MAX_VALUE};
018
019  private static final boolean[] REPEAT_UNDERSCORES = {false};
020
021  /**
022   * The raw version text.
023   */
024  private final String raw;
025
026  /**
027   * The ordering parts.
028   */
029  private final int[] ordering;
030
031  private final boolean[] underscores;
032
033  private final String comment;
034
035  /**
036   * Construct for "repeatable" version.
037   */
038  private MigrationVersion(String raw, String comment, boolean init) {
039    this.raw = raw;
040    this.comment = comment;
041    this.ordering = init ? REPEAT_ORDERING_MIN : REPEAT_ORDERING_MAX;
042    this.underscores = REPEAT_UNDERSCORES;
043  }
044
045  /**
046   * Construct for "normal" version.
047   */
048  private MigrationVersion(String raw, int[] ordering, boolean[] underscores, String comment) {
049    this.raw = raw;
050    this.ordering = ordering;
051    this.underscores = underscores;
052    this.comment = comment;
053  }
054
055  /**
056   * Return true if this is a "repeatable" version.
057   */
058  public boolean isRepeatable() {
059    return ordering == REPEAT_ORDERING_MIN || ordering == REPEAT_ORDERING_MAX;
060  }
061
062  /**
063   * Return the full version.
064   */
065  public String getFull() {
066    return raw;
067  }
068
069  public String toString() {
070    return raw;
071  }
072
073  /**
074   * Return the version comment.
075   */
076  public String getComment() {
077    return comment;
078  }
079
080  /**
081   * Return the version in raw form.
082   */
083  public String getRaw() {
084    return raw;
085  }
086
087  /**
088   * Return the trimmed version excluding version comment and un-parsable string.
089   */
090  public String asString() {
091    return formattedVersion(false, false);
092  }
093
094  /**
095   * Return the trimmed version with any underscores replaced with '.'
096   */
097  public String normalised() {
098    return formattedVersion(true, false);
099  }
100
101  /**
102   * Return the next version based on this version.
103   */
104  public String nextVersion() {
105    return formattedVersion(false, true);
106  }
107
108  /**
109   * Returns the version part of the string.
110   * <p>
111   * Normalised means always use '.' delimiters (no underscores).
112   * NextVersion means bump/increase the last version number by 1.
113   */
114  private String formattedVersion(boolean normalised, boolean nextVersion) {
115
116    if (isRepeatable()) {
117      return "R";
118    }
119    StringBuilder sb = new StringBuilder();
120    for (int i = 0; i < ordering.length; i++) {
121      if (i < ordering.length - 1) {
122        sb.append(ordering[i]);
123        if (normalised) {
124          sb.append('.');
125        } else {
126          sb.append(underscores[i] ? '_' : '.');
127        }
128      } else {
129        sb.append((nextVersion) ? ordering[i] + 1 : ordering[i]);
130      }
131    }
132    return sb.toString();
133  }
134
135  @Override
136  public int compareTo(MigrationVersion other) {
137
138    int otherLength = other.ordering.length;
139    for (int i = 0; i < ordering.length; i++) {
140      if (i >= otherLength) {
141        // considered greater
142        return 1;
143      }
144      if (ordering[i] != other.ordering[i]) {
145        return (ordering[i] > other.ordering[i]) ? 1 : -1;
146      }
147    }
148    if (ordering.length < otherLength) {
149      return -1;
150    }
151    return comment.compareTo(other.comment);
152  }
153
154  /**
155   * Parse the raw version string and just return the leading version number;
156   */
157  public static String trim(String raw) {
158    return parse(raw).asString();
159  }
160
161  /**
162   * Parse the raw version string into a MigrationVersion.
163   */
164  public static MigrationVersion parse(String raw) {
165
166    if (raw.startsWith("V") || raw.startsWith("v")) {
167      raw = raw.substring(1);
168    }
169
170    String comment = "";
171    String value = raw;
172    int commentStart = raw.indexOf("__");
173    if (commentStart > -1) {
174      // trim off the trailing comment
175      comment = raw.substring(commentStart + 2);
176      value = value.substring(0, commentStart);
177    }
178
179    value = value.replace('_', '.');
180
181    String[] sections = value.split("[\\.-]");
182
183    if (sections[0].startsWith("R") || sections[0].startsWith("r")) {
184      // a "repeatable" version (does not have a version number)
185      return new MigrationVersion(raw, comment, false);
186    }
187
188    if (sections[0].startsWith("I") || sections[0].startsWith("i")) {
189      // this script will be executed before all other scripts
190      return new MigrationVersion(raw, comment, true);
191    }
192
193    boolean[] underscores = new boolean[sections.length];
194    int[] ordering = new int[sections.length];
195
196    int delimiterPos = 0;
197    int stopIndex = 0;
198    for (int i = 0; i < sections.length; i++) {
199      try {
200        ordering[i] = Integer.parseInt(sections[i]);
201        stopIndex++;
202
203        delimiterPos += sections[i].length();
204        underscores[i] = (delimiterPos < raw.length() - 1 && raw.charAt(delimiterPos) == '_');
205        delimiterPos++;
206      } catch (NumberFormatException e) {
207        // stop parsing
208        logger.warn("The migrationscript '{}' contains non numeric version part. "
209            + "This may lead to misordered version scripts. NumberFormatException {}", raw, e.getMessage());
210        break;
211      }
212    }
213
214    int[] actualOrder = Arrays.copyOf(ordering, stopIndex);
215    boolean[] actualUnderscores = Arrays.copyOf(underscores, stopIndex);
216
217    return new MigrationVersion(raw, actualOrder, actualUnderscores, comment);
218  }
219
220}