View Javadoc
1   /*
2    *    Copyright 2010-2023 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.apache.ibatis.migration.commands;
17  
18  import java.io.IOException;
19  import java.io.Reader;
20  import java.math.BigDecimal;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.StringTokenizer;
27  
28  import org.apache.ibatis.migration.Change;
29  import org.apache.ibatis.migration.MigrationException;
30  import org.apache.ibatis.migration.hook.MigrationHook;
31  import org.apache.ibatis.migration.hook.ScriptHookContext;
32  import org.apache.ibatis.migration.operations.DatabaseOperation;
33  import org.apache.ibatis.migration.operations.StatusOperation;
34  import org.apache.ibatis.migration.options.SelectedOptions;
35  
36  public final class ScriptCommand extends BaseCommand {
37  
38    public ScriptCommand(SelectedOptions options) {
39      super(options);
40    }
41  
42    @Override
43    public void execute(String... sparams) {
44      try {
45        if (sparams == null || sparams.length < 1 || sparams[0] == null) {
46          throw new MigrationException("The script command requires a range of versions from v1 - v2.");
47        }
48        StringTokenizer parser = new StringTokenizer(sparams[0]);
49        int tokenCount = parser.countTokens();
50        boolean scriptPending = false;
51        boolean scriptPendingUndo = false;
52  
53        String firstToken = parser.nextToken();
54  
55        if (tokenCount == 1 && firstToken.equals("pending")) {
56          scriptPending = true;
57        } else if (tokenCount == 1 && firstToken.equals("pending_undo")) {
58          scriptPendingUndo = true;
59        } else if (!scriptPending && !scriptPendingUndo && tokenCount != 2) {
60          throw new MigrationException("The script command requires a range of versions from v1 - v2.");
61        }
62  
63        BigDecimal v1 = scriptPending || scriptPendingUndo ? null : new BigDecimal(firstToken);
64        BigDecimal v2 = scriptPending || scriptPendingUndo ? null : new BigDecimal(parser.nextToken());
65  
66        boolean undo;
67        undo = scriptPendingUndo;
68        if (!scriptPending && !scriptPendingUndo) {
69          int comparison = v1.compareTo(v2);
70          if (comparison == 0) {
71            throw new MigrationException(
72                "The script command requires two different versions. Use 0 to include the first version.");
73          }
74          undo = comparison > 0;
75        }
76  
77        Map<String, Object> hookBindings = new HashMap<>();
78        MigrationHook hook = createScriptHook();
79        List<Change> migrations = scriptPending || scriptPendingUndo ? new StatusOperation()
80            .operate(getConnectionProvider(), getMigrationLoader(), getDatabaseOperationOption(), null).getCurrentStatus()
81            : getMigrationLoader().getMigrations();
82        Collections.sort(migrations);
83        if (undo) {
84          Collections.reverse(migrations);
85        }
86        int count = 0;
87        for (int i = 0; i < migrations.size(); i++) {
88          Change change = migrations.get(i);
89          if (shouldRun(change, v1, v2, scriptPending || scriptPendingUndo)) {
90            if (count == 0 && hook != null) {
91              hookBindings.put(MigrationHook.HOOK_CONTEXT, new ScriptHookContext(null, undo));
92              hook.before(hookBindings);
93              printStream.println();
94            }
95            if (hook != null) {
96              hookBindings.put(MigrationHook.HOOK_CONTEXT, new ScriptHookContext(new Change(change), undo));
97              hook.beforeEach(hookBindings);
98              printStream.println();
99            }
100           printStream.println("-- " + change.getFilename());
101           try (Reader migrationReader = getMigrationLoader().getScriptReader(change, undo)) {
102             char[] cbuf = new char[1024];
103             int l;
104             while ((l = migrationReader.read(cbuf)) > -1) {
105               printStream.print(l == cbuf.length ? cbuf : Arrays.copyOf(cbuf, l));
106             }
107           }
108           count++;
109           printStream.println();
110           printStream.println();
111           if (!undo) {
112             printStream.println(generateVersionInsert(change));
113           } else if (i + 1 < migrations.size() || !DESC_CREATE_CHANGELOG.equals(change.getDescription())) {
114             printStream.println(generateVersionDelete(change));
115           }
116           printStream.println();
117           if (hook != null) {
118             hookBindings.put(MigrationHook.HOOK_CONTEXT, new ScriptHookContext(new Change(change), undo));
119             hook.afterEach(hookBindings);
120             printStream.println();
121           }
122         }
123       }
124       if (count > 0 && hook != null) {
125         hookBindings.put(MigrationHook.HOOK_CONTEXT, new ScriptHookContext(null, undo));
126         hook.after(hookBindings);
127         printStream.println();
128       }
129     } catch (IOException e) {
130       throw new MigrationException("Error generating script. Cause: " + e, e);
131     }
132   }
133 
134   private String generateVersionInsert(Change change) {
135     return "INSERT INTO " + changelogTable() + " (ID, APPLIED_AT, DESCRIPTION) " + "VALUES (" + change.getId() + ", '"
136         + DatabaseOperation.generateAppliedTimeStampAsString() + "', '" + change.getDescription().replace('\'', ' ')
137         + "')" + getDelimiter();
138   }
139 
140   private String generateVersionDelete(Change change) {
141     return "DELETE FROM " + changelogTable() + " WHERE ID = " + change.getId() + getDelimiter();
142   }
143 
144   private boolean shouldRun(Change change, BigDecimal v1, BigDecimal v2, boolean pendingOnly) {
145     if (pendingOnly) {
146       return change.getAppliedTimestamp() == null;
147     }
148     BigDecimal id = change.getId();
149     if (v1.compareTo(v2) > 0) {
150       return id.compareTo(v2) > 0 && id.compareTo(v1) <= 0;
151     }
152     return id.compareTo(v1) > 0 && id.compareTo(v2) <= 0;
153   }
154 
155   // Issue 699
156   private String getDelimiter() {
157     StringBuilder delimiter = new StringBuilder();
158     if (environment().isFullLineDelimiter()) {
159       delimiter.append('\n');
160     }
161     delimiter.append(environment().getDelimiter());
162     return delimiter.toString();
163   }
164 
165   private MigrationHook createScriptHook() {
166     String before = environment().getHookBeforeScript();
167     String beforeEach = environment().getHookBeforeEachScript();
168     String afterEach = environment().getHookAfterEachScript();
169     String after = environment().getHookAfterScript();
170     if (before == null && beforeEach == null && afterEach == null && after == null) {
171       return null;
172     }
173     return createFileMigrationHook(before, beforeEach, afterEach, after);
174   }
175 }