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;
17  
18  import java.io.File;
19  import java.io.FileInputStream;
20  import java.io.FilterReader;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.nio.charset.Charset;
26  import java.util.Properties;
27  
28  public class MigrationReader extends FilterReader {
29  
30    // Note: Cannot use lineSeparator directly at this time due to tests manipulating it
31    private final String lineSeparator = System.getProperty("line.separator", "\n");
32  
33    private static final String UNDO_TAG = "@UNDO";
34  
35    private boolean undo;
36  
37    private Properties variables;
38  
39    private Part part = Part.NEW_LINE;
40  
41    private VariableStatus variableStatus = VariableStatus.NOTHING;
42  
43    private char previousChar;
44  
45    private int undoIndex;
46  
47    private int afterCommentPrefixIndex;
48  
49    private int afterDoubleSlashIndex;
50  
51    private boolean inUndo;
52  
53    private final StringBuilder buffer = new StringBuilder();
54  
55    private final StringBuilder lineBuffer = new StringBuilder();
56  
57    private final VariableReplacer replacer;
58  
59    private enum Part {
60      NOT_UNDO_LINE,
61  
62      NEW_LINE,
63  
64      COMMENT_PREFIX,
65  
66      AFTER_COMMENT_PREFIX,
67  
68      DOUBLE_SLASH,
69  
70      AFTER_DOUBLE_SLASH,
71  
72      UNDO_TAG,
73  
74      AFTER_UNDO_TAG
75    }
76  
77    private enum VariableStatus {
78      NOTHING,
79  
80      FOUND_DOLLAR,
81  
82      FOUND_OPEN_BRACE,
83  
84      FOUND_POSSIBLE_VARIABLE
85    }
86  
87    public MigrationReader(File file, String charset, boolean undo, Properties variables) throws IOException {
88      this(new FileInputStream(file), charset, undo, variables);
89    }
90  
91    public MigrationReader(InputStream inputStream, String charset, boolean undo, Properties variables) {
92      super(scriptFileReader(inputStream, charset));
93      this.undo = undo;
94      this.variables = variables;
95      replacer = new VariableReplacer(this.variables);
96    }
97  
98    @Override
99    public int read(char[] cbuf, int off, int len) throws IOException {
100     if (!undo && inUndo) {
101       if (buffer.length() > 0) {
102         return readFromBuffer(cbuf, off, len);
103       }
104       return -1;
105     }
106     while (buffer.length() == 0) {
107       int result = in.read(cbuf, off, len);
108       if (result == -1) {
109         if (lineBuffer.length() > 0 && (!undo || inUndo)) {
110           addToBuffer(lineBuffer);
111         }
112         if (buffer.length() > 0) {
113           break;
114         }
115         return -1;
116       }
117 
118       for (int i = off; i < off + result; i++) {
119         char c = cbuf[i];
120 
121         determinePart(c);
122         searchVariable(c);
123 
124         if (c == '\r' || c == '\n' && previousChar != '\r') {
125           switch (part) {
126             case AFTER_UNDO_TAG:
127               if (!undo) {
128                 // Won't read from the file anymore.
129                 lineBuffer.setLength(0);
130                 int bufferLen = buffer.length();
131                 if (bufferLen == 0) {
132                   return -1;
133                 }
134                 return readFromBuffer(cbuf, off, len);
135               }
136               addToBuffer(lineBuffer.delete(afterCommentPrefixIndex, afterDoubleSlashIndex)
137                   .insert(afterCommentPrefixIndex, ' '));
138               inUndo = true;
139               break;
140             case NOT_UNDO_LINE:
141               if (!undo || inUndo) {
142                 addToBuffer(lineBuffer);
143               } else {
144                 lineBuffer.setLength(0);
145               }
146               break;
147             default:
148               break;
149           }
150           part = Part.NEW_LINE;
151         } else if (c == '\n') {
152           // LF after CR
153           part = Part.NEW_LINE;
154         } else {
155           lineBuffer.append(c);
156         }
157         previousChar = c;
158       }
159     }
160     return readFromBuffer(cbuf, off, len);
161   }
162 
163   private void addToBuffer(StringBuilder line) {
164     replaceVariables(line);
165     buffer.append(line).append(lineSeparator);
166     lineBuffer.setLength(0);
167   }
168 
169   private void replaceVariables(StringBuilder line) {
170     if (variableStatus == VariableStatus.FOUND_POSSIBLE_VARIABLE) {
171       String lineBufferStr = line.toString();
172       String processed = replacer.replace(lineBufferStr);
173       if (!lineBufferStr.equals(processed)) {
174         line.setLength(0);
175         line.append(processed);
176       }
177     }
178     variableStatus = VariableStatus.NOTHING;
179   }
180 
181   private int readFromBuffer(char[] cbuf, int off, int len) {
182     int bufferLen = buffer.length();
183     int read = bufferLen > len ? len : bufferLen;
184     buffer.getChars(0, read, cbuf, off);
185     buffer.delete(0, read);
186     return read;
187   }
188 
189   private void determinePart(char c) {
190     switch (part) {
191       case NEW_LINE:
192         if (inUndo) {
193           part = Part.NOT_UNDO_LINE;
194         } else if (c == 0x09 || c == 0x20) {
195           // ignore whitespace
196         } else if (c == '/' || c == '-') {
197           part = Part.COMMENT_PREFIX;
198         } else {
199           part = Part.NOT_UNDO_LINE;
200         }
201         break;
202       case COMMENT_PREFIX:
203         if ((c == '/' || c == '-') && c == previousChar) {
204           part = Part.AFTER_COMMENT_PREFIX;
205           afterCommentPrefixIndex = lineBuffer.length() + 1;
206         } else {
207           part = Part.NOT_UNDO_LINE;
208         }
209         break;
210       case AFTER_COMMENT_PREFIX:
211         if (c == 0x09 || c == 0x20) {
212           // ignore whitespace
213         } else if (c == '/') {
214           part = Part.DOUBLE_SLASH;
215         } else {
216           part = Part.NOT_UNDO_LINE;
217         }
218         break;
219       case DOUBLE_SLASH:
220         if (c == '/' && c == previousChar) {
221           part = Part.AFTER_DOUBLE_SLASH;
222           afterDoubleSlashIndex = lineBuffer.length() + 1;
223           undoIndex = 0;
224         } else {
225           part = Part.NOT_UNDO_LINE;
226         }
227         break;
228       case AFTER_DOUBLE_SLASH:
229         if (c == 0x09 || c == 0x20) {
230           // ignore whitespace
231         } else if (c == UNDO_TAG.charAt(undoIndex)) {
232           part = Part.UNDO_TAG;
233           undoIndex = 1;
234         } else {
235           part = Part.NOT_UNDO_LINE;
236         }
237         break;
238       case UNDO_TAG:
239         if (c != UNDO_TAG.charAt(undoIndex)) {
240           part = Part.NOT_UNDO_LINE;
241         } else if (++undoIndex >= UNDO_TAG.length()) {
242           part = Part.AFTER_UNDO_TAG;
243         }
244         break;
245       default:
246         break;
247     }
248   }
249 
250   private void searchVariable(char c) {
251     // This is just a quick check.
252     switch (variableStatus) {
253       case NOTHING:
254         if ((part == Part.NOT_UNDO_LINE || part == Part.AFTER_UNDO_TAG) && c == '$') {
255           variableStatus = VariableStatus.FOUND_DOLLAR;
256         }
257         break;
258       case FOUND_DOLLAR:
259         variableStatus = c == '{' ? VariableStatus.FOUND_OPEN_BRACE : VariableStatus.NOTHING;
260         break;
261       case FOUND_OPEN_BRACE:
262         if (c == '}') {
263           variableStatus = VariableStatus.FOUND_POSSIBLE_VARIABLE;
264         }
265         break;
266       default:
267         break;
268     }
269   }
270 
271   @Override
272   public int read() throws IOException {
273     char[] buf = new char[1];
274     int result = read(buf, 0, 1);
275     return result == -1 ? -1 : (int) buf[0];
276   }
277 
278   protected static Reader scriptFileReader(InputStream inputStream, String charset) {
279     if (charset == null || charset.length() == 0) {
280       return new InputStreamReader(inputStream);
281     }
282     return new InputStreamReader(inputStream, Charset.forName(charset));
283   }
284 }