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