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