1 /*
2  * Copyright 2010-2020 Redgate Software Ltd
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  *         http://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.flywaydb.core.api;
17
18 import org.flywaydb.core.api.logging.Log;
19 import org.flywaydb.core.api.logging.LogFactory;
20
21 import java.io.File;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 /**
26  * A location to load migrations from.
27  */

28 public final class Location implements Comparable<Location> {
29     private static final Log LOG = LogFactory.getLog(Location.class);
30
31     /**
32      * The prefix for classpath locations.
33      */

34     private static final String CLASSPATH_PREFIX = "classpath:";
35
36     /**
37      * The prefix for filesystem locations.
38      */

39     public static final String FILESYSTEM_PREFIX = "filesystem:";
40
41     /**
42      * The prefix part of the location. Can be either classpath: or filesystem:.
43      */

44     private final String prefix;
45
46     /**
47      * The path part of the location.
48      */

49     private String rawPath;
50
51     /**
52      * The first folder in the path. This will equal rawPath if the path does not contain any wildcards
53      */

54     private String rootPath;
55
56     private Pattern pathRegex = null;
57
58     /**
59      * Creates a new location.
60      *
61      * @param descriptor The location descriptor.
62      */

63     public Location(String descriptor) {
64         String normalizedDescriptor = descriptor.trim();
65
66         if (normalizedDescriptor.contains(":")) {
67             prefix = normalizedDescriptor.substring(0, normalizedDescriptor.indexOf(":") + 1);
68             rawPath = normalizedDescriptor.substring(normalizedDescriptor.indexOf(":") + 1);
69         } else {
70             prefix = CLASSPATH_PREFIX;
71             rawPath = normalizedDescriptor;
72         }
73
74         if (isClassPath()) {
75             if (rawPath.contains(".")) {
76                 LOG.warn("Use of dots (.) as path separators will be deprecated in Flyway 7. Path: " + rawPath);
77             }
78             rawPath = rawPath.replace(".""/");
79             if (rawPath.startsWith("/")) {
80                 rawPath = rawPath.substring(1);
81             }
82             if (rawPath.endsWith("/")) {
83                 rawPath = rawPath.substring(0, rawPath.length() - 1);
84             }
85             processRawPath();
86         } else if (isFileSystem()) {
87             processRawPath();
88             rootPath = new File(rootPath).getPath();
89
90             if (pathRegex == null) {
91                 // if the original path contained no wildcards, also normalise it
92                 rawPath = new File(rawPath).getPath();
93             }
94         } else {
95             throw new FlywayException("Unknown prefix for location (should be either filesystem: or classpath:): "
96                     + normalizedDescriptor);
97         }
98
99         if (rawPath.endsWith(File.separator)) {
100             rawPath = rawPath.substring(0, rawPath.length() - 1);
101         }
102     }
103
104     /**
105      * Process the rawPath into a rootPath and a regex.
106      * Supported wildcards:
107      * **: Match any 0 or more directories
108      * *: Match any sequence of non-seperator characters
109      * ?: Match any single character
110      */

111     private void processRawPath() {
112         if (rawPath.contains("*") || rawPath.contains("?")) {
113             // we need to figure out the root, and create the regex
114
115             String seperator = isFileSystem() ? File.separator : "/";
116             String escapedSeperator = seperator.replace("\\""\\\\").replace("/""\\/");
117
118             // split on either of the path seperators
119             String[] pathSplit = rawPath.split("[\\\\/]");
120
121             StringBuilder rootPart = new StringBuilder();
122             StringBuilder patternPart = new StringBuilder();
123
124             boolean endsInFile = false;
125             boolean skipSeperator = false;
126             boolean inPattern = false;
127             for (String pathPart : pathSplit) {
128                 endsInFile = false;
129
130                 if (pathPart.contains("*") || pathPart.contains("?")) {
131                     inPattern = true;
132                 }
133
134                 if (inPattern) {
135                     if (skipSeperator) {
136                         skipSeperator = false;
137                     } else {
138                         patternPart.append("/");
139                     }
140
141                     String regex;
142                     if ("**".equals(pathPart)) {
143                         regex = "([^/]+/)*?";
144
145                         // this pattern contains the ending seperator, so make sure we skip appending it after
146                         skipSeperator = true;
147                     } else {
148                         endsInFile = pathPart.contains(".");
149
150                         regex = pathPart;
151                         regex = regex.replace(".""\\.");
152                         regex = regex.replace("?""[^/]");
153                         regex = regex.replace("*""[^/]+?");
154                     }
155
156                     patternPart.append(regex);
157                 } else {
158                     rootPart.append(seperator).append(pathPart);
159                 }
160             }
161
162             // We always append a seperator before each part, so ensure we skip it when setting the final rootPath
163             rootPath = rootPart.length() > 0 ? rootPart.toString().substring(1) : "";
164
165             // Again, skip first seperator
166             String pattern = patternPart.toString().substring(1);
167
168             // Replace the temporary / with the actual escaped seperator
169             pattern = pattern.replace("/", escapedSeperator);
170
171             // Append the rootpath if it is non-empty
172             if (rootPart.length() > 0) {
173                 pattern = rootPath.replace(seperator, escapedSeperator) + escapedSeperator + pattern;
174             }
175
176             // if the path did not end in a file, then append the file match pattern
177             if (!endsInFile) {
178                 pattern = pattern + escapedSeperator + "(?<relpath>.*)";
179             }
180
181             pathRegex = Pattern.compile(pattern);
182         } else {
183             rootPath = rawPath;
184         }
185     }
186
187     /**
188      * @return Whether the given path matches this locations regex. Will always return true when the location did not contain any wildcards.
189      */

190     public boolean matchesPath(String path) {
191         if (pathRegex == null) {
192             return true;
193         }
194
195         return pathRegex.matcher(path).matches();
196     }
197
198     /**
199      * Returns the path relative to this location. If the location path contains wildcards, the returned path will be relative
200      * to the last non-wildcard folder in the path.
201      * @return the path relative to this location
202      */

203     public String getPathRelativeToThis(String path) {
204         if (pathRegex != null && pathRegex.pattern().contains("?<relpath>")) {
205             Matcher matcher = pathRegex.matcher(path);
206             if (matcher.matches()) {
207                 String relPath = matcher.group("relpath");
208                 if (relPath != null && relPath.length() > 0) {
209                     return relPath;
210                 }
211             }
212         }
213
214         return rootPath.length() > 0 ? path.substring(rootPath.length() + 1) : path;
215     }
216
217     /**
218      * Checks whether this denotes a location on the classpath.
219      *
220      * @return {@code trueif it does, {@code falseif it doesn't.
221      */

222     public boolean isClassPath() {
223         return CLASSPATH_PREFIX.equals(prefix);
224     }
225
226     /**
227      * Checks whether this denotes a location on the filesystem.
228      *
229      * @return {@code trueif it does, {@code falseif it doesn't.
230      */

231     public boolean isFileSystem() {
232         return FILESYSTEM_PREFIX.equals(prefix);
233     }
234
235     /**
236      * Checks whether this location is a parent of this other location.
237      *
238      * @param other The other location.
239      * @return {@code trueif it is, {@code falseif it isn't.
240      */

241     @SuppressWarnings("SimplifiableIfStatement")
242     public boolean isParentOf(Location other) {
243         if (pathRegex != null || other.pathRegex != null) {
244             return false;
245         }
246
247         if (isClassPath() && other.isClassPath()) {
248             return (other.getDescriptor() + "/").startsWith(getDescriptor() + "/");
249         }
250         if (isFileSystem() && other.isFileSystem()) {
251             return (other.getDescriptor() + File.separator).startsWith(getDescriptor() + File.separator);
252         }
253         return false;
254     }
255
256     /**
257      * @return The prefix part of the location. Can be either classpath: or filesystem:.
258      */

259     public String getPrefix() {
260         return prefix;
261     }
262
263     /**
264      * @return The root part of the path part of the location.
265      */

266     public String getRootPath() {
267         return rootPath;
268     }
269
270     /**
271      * @return The path part of the location.
272      */

273     public String getPath() {
274         return rawPath;
275     }
276
277     /**
278      * @return The the regex that matches in original path. Null if the original path did not contain any wildcards.
279      */

280     public Pattern getPathRegex() {
281         return pathRegex;
282     }
283
284     /**
285      * @return The complete location descriptor.
286      */

287     public String getDescriptor() {
288         return prefix + rawPath;
289     }
290
291     @SuppressWarnings("NullableProblems")
292     public int compareTo(Location o) {
293         return getDescriptor().compareTo(o.getDescriptor());
294     }
295
296     @Override
297     public boolean equals(Object o) {
298         if (this == o) return true;
299         if (o == null || getClass() != o.getClass()) return false;
300
301         Location location = (Location) o;
302
303         return getDescriptor().equals(location.getDescriptor());
304     }
305
306     @Override
307     public int hashCode() {
308         return getDescriptor().hashCode();
309     }
310
311     /**
312      * @return The complete location descriptor.
313      */

314     @Override
315     public String toString() {
316         return getDescriptor();
317     }
318 }