001package io.ebeaninternal.dbmigration.model;
002
003import io.ebean.migration.MigrationVersion;
004import io.ebeaninternal.dbmigration.migration.ChangeSet;
005import io.ebeaninternal.dbmigration.migration.ChangeSetType;
006import io.ebeaninternal.dbmigration.migration.DropColumn;
007import io.ebeaninternal.dbmigration.migration.DropHistoryTable;
008import io.ebeaninternal.dbmigration.migration.DropTable;
009import io.ebeaninternal.dbmigration.migration.Migration;
010
011import java.util.ArrayList;
012import java.util.Iterator;
013import java.util.LinkedHashMap;
014import java.util.List;
015
016/**
017 * The migrations with pending un-applied drops.
018 */
019public class PendingDrops {
020
021  private final LinkedHashMap<String, Entry> map = new LinkedHashMap<>();
022
023  /**
024   * Add a 'pending drops' changeSet for the given version.
025   */
026  public void add(MigrationVersion version, ChangeSet changeSet) {
027
028    Entry entry = map.computeIfAbsent(version.normalised(), k -> new Entry(version));
029    entry.add(changeSet);
030  }
031
032  /**
033   * Return the list of versions with pending drops.
034   */
035  public List<String> pendingDrops() {
036
037    List<String> versions = new ArrayList<>();
038    for (Entry value : map.values()) {
039      if (value.hasPendingDrops()) {
040        versions.add(value.version.asString());
041      }
042    }
043    return versions;
044  }
045
046  /**
047   * All the pending drops for this migration version have been applied so we need
048   * to remove the (unsuppressed) pending drops for this version.
049   */
050  public boolean appliedDropsFor(ChangeSet changeSet) {
051
052    MigrationVersion version = MigrationVersion.parse(changeSet.getDropsFor());
053
054    Entry entry = map.get(version.normalised());
055    if (entry.removeDrops(changeSet)) {
056      // it had no suppressForever changeSets so remove completely
057      map.remove(version.normalised());
058      return true;
059    }
060
061    return false;
062  }
063
064  /**
065   * Return the migration for the pending drops from a version.
066   * <p>
067   * The value of version can be "next" to find the first un-applied pending drops.
068   * </p>
069   */
070  public Migration migrationForVersion(String pendingVersion) {
071
072    Entry entry = getEntry(pendingVersion);
073
074    Migration migration = new Migration();
075    Iterator<ChangeSet> it = entry.list.iterator();
076    while (it.hasNext()) {
077      ChangeSet changeSet = it.next();
078      if (!isSuppressForever(changeSet)) {
079        it.remove();
080        changeSet.setType(ChangeSetType.APPLY);
081        changeSet.setDropsFor(entry.version.asString());
082        migration.getChangeSet().add(changeSet);
083      }
084    }
085
086    if (migration.getChangeSet().isEmpty()) {
087      throw new IllegalArgumentException("The remaining pendingDrops changeSets in migration [" + pendingVersion + "] are suppressDropsForever=true and can't be applied");
088    }
089
090    if (!entry.containsSuppressForever()) {
091      // we can remove it completely as it has no suppressForever changes
092      map.remove(entry.version.normalised());
093    }
094
095    return migration;
096  }
097
098  private Entry getEntry(String pendingVersion) {
099
100    if ("next".equalsIgnoreCase(pendingVersion)) {
101      Iterator<Entry> it = map.values().iterator();
102      if (it.hasNext()) {
103        return it.next();
104      }
105    } else {
106      Entry remove = map.get(MigrationVersion.parse(pendingVersion).normalised());
107      if (remove != null) {
108        return remove;
109      }
110    }
111    throw new IllegalArgumentException("No 'pendingDrops' changeSets for migration version [" + pendingVersion + "] found");
112  }
113
114  /**
115   * Register pending drop columns on history tables to the new model.
116   */
117  public void registerPendingHistoryDropColumns(ModelContainer newModel) {
118
119    for (Entry entry : map.values()) {
120      for (ChangeSet changeSet : entry.list) {
121        newModel.registerPendingHistoryDropColumns(changeSet);
122      }
123    }
124  }
125
126  /**
127   * Return true if there is an Entry for the given version.
128   */
129  boolean testContainsEntryFor(MigrationVersion version) {
130    return map.containsKey(version.normalised());
131  }
132
133  /**
134   * Return the Entry for the given version.
135   */
136  Entry testGetEntryFor(MigrationVersion version) {
137    return map.get(version.normalised());
138  }
139
140  static class Entry {
141
142    final MigrationVersion version;
143
144    final List<ChangeSet> list = new ArrayList<>();
145
146    Entry(MigrationVersion version) {
147      this.version = version;
148    }
149
150    void add(ChangeSet changeSet) {
151      list.add(changeSet);
152    }
153
154    /**
155     * Return true if this contains suppressForever changeSets.
156     */
157    boolean containsSuppressForever() {
158      for (ChangeSet changeSet : list) {
159        if (isSuppressForever(changeSet)) {
160          return true;
161        }
162      }
163      return false;
164    }
165
166    /**
167     * Return true if this contains drops that can be applied / migrated.
168     */
169    boolean hasPendingDrops() {
170      for (ChangeSet changeSet : list) {
171        if (!isSuppressForever(changeSet)) {
172          return true;
173        }
174      }
175      return false;
176    }
177
178    /**
179     * Remove the drops that are not suppressForever and return true if that
180     * removed all the changeSets (and there are no suppressForever ones).
181     */
182    boolean removeDrops(ChangeSet appliedDrops) {
183
184      Iterator<ChangeSet> iterator = list.iterator();
185      while (iterator.hasNext()) {
186        ChangeSet next = iterator.next();
187        if (!isSuppressForever(next)) {
188          removeMatchingChanges(next, appliedDrops);
189          if (next.getChangeSetChildren().isEmpty()) {
190            iterator.remove();
191          }
192        }
193      }
194
195      return list.isEmpty();
196    }
197
198    /**
199     * Remove the applied drops from the pending ones matching by table name and column name.
200     */
201    private void removeMatchingChanges(ChangeSet pendingDrops, ChangeSet appliedDrops) {
202
203      List<Object> pending = pendingDrops.getChangeSetChildren();
204      Iterator<Object> iterator = pending.iterator();
205      while (iterator.hasNext()) {
206        Object pendingDrop = iterator.next();
207        if (pendingDrop instanceof DropColumn && dropColumnIn((DropColumn) pendingDrop, appliedDrops)) {
208          iterator.remove();
209
210        } else if (pendingDrop instanceof DropTable && dropTableIn((DropTable) pendingDrop, appliedDrops)) {
211          iterator.remove();
212
213        } else if (pendingDrop instanceof DropHistoryTable && dropHistoryTableIn((DropHistoryTable) pendingDrop, appliedDrops)) {
214          iterator.remove();
215
216        }
217      }
218    }
219
220    /**
221     * Return true if the pendingDrop is contained in the appliedDrops.
222     */
223    private boolean dropHistoryTableIn(DropHistoryTable pendingDrop, ChangeSet appliedDrops) {
224      for (Object o : appliedDrops.getChangeSetChildren()) {
225        if (o instanceof DropHistoryTable && sameHistoryTable(pendingDrop, (DropHistoryTable) o)) {
226          return true;
227        }
228      }
229      return false;
230    }
231
232    /**
233     * Return true if the pendingDrop is contained in the appliedDrops.
234     */
235    private boolean dropTableIn(DropTable pendingDrop, ChangeSet appliedDrops) {
236      for (Object o : appliedDrops.getChangeSetChildren()) {
237        if (o instanceof DropTable && sameTable(pendingDrop, (DropTable) o)) {
238          return true;
239        }
240      }
241      return false;
242    }
243
244    /**
245     * Return true if the pendingDrop is contained in the appliedDrops.
246     */
247    private boolean dropColumnIn(DropColumn pendingDrop, ChangeSet appliedDrops) {
248      for (Object o : appliedDrops.getChangeSetChildren()) {
249        if (o instanceof DropColumn && sameColumn(pendingDrop, (DropColumn) o)) {
250          return true;
251        }
252      }
253      return false;
254    }
255
256    /**
257     * Return true if the DropHistoryTable match by base-table name.
258     */
259    private boolean sameHistoryTable(DropHistoryTable pendingDrop, DropHistoryTable o) {
260      return pendingDrop.getBaseTable().equals(o.getBaseTable());
261    }
262
263    /**
264     * Return true if the DropTable match by table name.
265     */
266    private boolean sameTable(DropTable pendingDrop, DropTable o) {
267      return pendingDrop.getName().equals(o.getName());
268    }
269
270    /**
271     * Return true if the DropColumns match by table and column name.
272     */
273    private boolean sameColumn(DropColumn pending, DropColumn o) {
274      return pending.getColumnName().equals(o.getColumnName())
275        && pending.getTableName().equals(o.getTableName());
276    }
277
278  }
279
280  private static boolean isSuppressForever(ChangeSet next) {
281    return Boolean.TRUE.equals(next.isSuppressDropsForever());
282  }
283
284}