• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / migrations / 718

23 May 2025 05:07PM UTC coverage: 80.917% (+0.04%) from 80.873%
718

push

github

web-flow
Merge pull request #448 from harawata/use-nio

Use NIO to make modernizer happy

692 of 986 branches covered (70.18%)

20 of 28 new or added lines in 11 files covered. (71.43%)

1836 of 2269 relevant lines covered (80.92%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.22
/src/main/java/org/apache/ibatis/migration/io/DefaultVFS.java
1
/*
2
 *    Copyright 2010-2025 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.io;
17

18
import java.io.BufferedReader;
19
import java.io.File;
20
import java.io.FileNotFoundException;
21
import java.io.IOException;
22
import java.io.InputStream;
23
import java.io.InputStreamReader;
24
import java.net.MalformedURLException;
25
import java.net.URL;
26
import java.net.URLEncoder;
27
import java.nio.charset.StandardCharsets;
28
import java.nio.file.InvalidPathException;
29
import java.nio.file.Path;
30
import java.util.ArrayList;
31
import java.util.Arrays;
32
import java.util.List;
33
import java.util.jar.JarEntry;
34
import java.util.jar.JarInputStream;
35
import java.util.logging.Level;
36
import java.util.logging.Logger;
37

38
/**
39
 * A default implementation of {@link VFS} that works for most application servers.
40
 *
41
 * @author Ben Gunter
42
 */
43
public class DefaultVFS extends VFS {
1✔
44
  private static final Logger log = Logger.getLogger(DefaultVFS.class.getName());
1✔
45

46
  /** The magic header that indicates a JAR (ZIP) file. */
47
  private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };
1✔
48

49
  @Override
50
  public boolean isValid() {
51
    return true;
1✔
52
  }
53

54
  @Override
55
  public List<String> list(URL url, String path) throws IOException {
56
    InputStream is = null;
1✔
57
    try {
58
      List<String> resources = new ArrayList<>();
1✔
59

60
      // First, try to find the URL of a JAR file containing the requested resource. If a JAR
61
      // file is found, then we'll list child resources by reading the JAR.
62
      URL jarUrl = findJarForResource(url);
1✔
63
      if (jarUrl != null) {
1!
64
        is = jarUrl.openStream();
×
65
        if (log.isLoggable(Level.FINER)) {
×
66
          log.log(Level.FINER, "Listing " + url);
×
67
        }
68
        resources = listResources(new JarInputStream(is), path);
×
69
      } else {
70
        List<String> children = new ArrayList<>();
1✔
71
        try {
72
          if (isJar(url)) {
1!
73
            // Some versions of JBoss VFS might give a JAR stream even if the resource
74
            // referenced by the URL isn't actually a JAR
75
            is = url.openStream();
×
76
            try (JarInputStream jarInput = new JarInputStream(is)) {
×
77
              if (log.isLoggable(Level.FINER)) {
×
78
                log.log(Level.FINER, "Listing " + url);
×
79
              }
80
              for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
×
81
                if (log.isLoggable(Level.FINER)) {
×
82
                  log.log(Level.FINER, "Jar entry: " + entry.getName());
×
83
                }
84
                children.add(entry.getName());
×
85
              }
86
            }
87
          } else {
88
            /*
89
             * Some servlet containers allow reading from directory resources like a text file, listing the child
90
             * resources one per line. However, there is no way to differentiate between directory and file resources
91
             * just by reading them. To work around that, as each line is read, try to look it up via the class loader
92
             * as a child of the current resource. If any line fails then we assume the current resource is not a
93
             * directory.
94
             */
95
            is = url.openStream();
1✔
96
            List<String> lines = new ArrayList<>();
1✔
97
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
1✔
98
              for (String line; (line = reader.readLine()) != null;) {
1✔
99
                if (log.isLoggable(Level.FINER)) {
1!
100
                  log.log(Level.FINER, "Reader entry: " + line);
×
101
                }
102
                lines.add(line);
1✔
103
                if (getResources(path + "/" + line).isEmpty()) {
1✔
104
                  lines.clear();
1✔
105
                  break;
1✔
106
                }
107
              }
108
            } catch (InvalidPathException e) {
×
109
              // #1974
110
              lines.clear();
×
111
            }
1✔
112
            if (!lines.isEmpty()) {
1✔
113
              if (log.isLoggable(Level.FINER)) {
1!
114
                log.log(Level.FINER, "Listing " + url);
×
115
              }
116
              children.addAll(lines);
1✔
117
            }
118
          }
119
        } catch (FileNotFoundException e) {
×
120
          /*
121
           * For file URLs the openStream() call might fail, depending on the servlet container, because directories
122
           * can't be opened for reading. If that happens, then list the directory directly instead.
123
           */
124
          if (!"file".equals(url.getProtocol())) {
×
125
            // No idea where the exception came from so rethrow it
126
            throw e;
×
127
          }
NEW
128
          File file = Path.of(url.getFile()).toFile();
×
129
          if (log.isLoggable(Level.FINER)) {
×
130
            log.log(Level.FINER, "Listing directory " + file.getAbsolutePath());
×
131
          }
132
          if (file.isDirectory()) {
×
133
            if (log.isLoggable(Level.FINER)) {
×
134
              log.log(Level.FINER, "Listing " + url);
×
135
            }
136
            children = Arrays.asList(file.list());
×
137
          }
138
        }
1✔
139

140
        // The URL prefix to use when recursively listing child resources
141
        String prefix = url.toExternalForm();
1✔
142
        if (!prefix.endsWith("/")) {
1!
143
          prefix = prefix + "/";
1✔
144
        }
145

146
        // Iterate over immediate children, adding files and recurring into directories
147
        for (String child : children) {
1✔
148
          String resourcePath = path + "/" + child;
1✔
149
          resources.add(resourcePath);
1✔
150
          URL childUrl = new URL(prefix + child);
1✔
151
          resources.addAll(list(childUrl, resourcePath));
1✔
152
        }
1✔
153
      }
154

155
      return resources;
1✔
156
    } finally {
157
      if (is != null) {
1!
158
        try {
159
          is.close();
1✔
160
        } catch (Exception e) {
×
161
          // Ignore
162
        }
1✔
163
      }
164
    }
165
  }
166

167
  /**
168
   * List the names of the entries in the given {@link JarInputStream} that begin with the specified {@code path}.
169
   * Entries will match with or without a leading slash.
170
   *
171
   * @param jar
172
   *          The JAR input stream
173
   * @param path
174
   *          The leading path to match
175
   *
176
   * @return The names of all the matching entries
177
   *
178
   * @throws IOException
179
   *           If I/O errors occur
180
   */
181
  protected List<String> listResources(JarInputStream jar, String path) throws IOException {
182
    // Include the leading and trailing slash when matching names
183
    if (!path.startsWith("/")) {
×
184
      path = '/' + path;
×
185
    }
186
    if (!path.endsWith("/")) {
×
187
      path = path + '/';
×
188
    }
189

190
    // Iterate over the entries and collect those that begin with the requested path
191
    List<String> resources = new ArrayList<>();
×
192
    for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
×
193
      if (!entry.isDirectory()) {
×
194
        // Add leading slash if it's missing
195
        StringBuilder name = new StringBuilder(entry.getName());
×
196
        if (name.charAt(0) != '/') {
×
197
          name.insert(0, '/');
×
198
        }
199

200
        // Check file name
201
        if (name.indexOf(path) == 0) {
×
202
          if (log.isLoggable(Level.FINER)) {
×
203
            log.log(Level.FINER, "Found resource: " + name);
×
204
          }
205
          // Trim leading slash
206
          resources.add(name.substring(1));
×
207
        }
208
      }
×
209
    }
210
    return resources;
×
211
  }
212

213
  /**
214
   * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced by the URL. That is,
215
   * assuming the URL references a JAR entry, this method will return a URL that references the JAR file containing the
216
   * entry. If the JAR cannot be located, then this method returns null.
217
   *
218
   * @param url
219
   *          The URL of the JAR entry.
220
   *
221
   * @return The URL of the JAR file, if one is found. Null if not.
222
   *
223
   * @throws MalformedURLException
224
   *           the malformed URL exception
225
   */
226
  protected URL findJarForResource(URL url) throws MalformedURLException {
227
    if (log.isLoggable(Level.FINER)) {
1!
228
      log.log(Level.FINER, "Find JAR URL: " + url);
×
229
    }
230

231
    // If the file part of the URL is itself a URL, then that URL probably points to the JAR
232
    boolean continueLoop = true;
1✔
233
    while (continueLoop) {
1✔
234
      try {
235
        url = new URL(url.getFile());
×
236
        if (log.isLoggable(Level.FINER)) {
×
237
          log.log(Level.FINER, "Inner URL: " + url);
×
238
        }
239
      } catch (MalformedURLException e) {
1✔
240
        // This will happen at some point and serves as a break in the loop
241
        continueLoop = false;
1✔
242
      }
1✔
243
    }
244

245
    // Look for the .jar extension and chop off everything after that
246
    StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
1✔
247
    int index = jarUrl.lastIndexOf(".jar");
1✔
248
    if (index < 0) {
1!
249
      if (log.isLoggable(Level.FINER)) {
1!
250
        log.log(Level.FINER, "Not a JAR: " + jarUrl);
×
251
      }
252
      return null;
1✔
253
    }
254
    jarUrl.setLength(index + 4);
×
255
    if (log.isLoggable(Level.FINER)) {
×
256
      log.log(Level.FINER, "Extracted JAR URL: " + jarUrl);
×
257
    }
258

259
    // Try to open and test it
260
    try {
261
      URL testUrl = new URL(jarUrl.toString());
×
262
      if (isJar(testUrl)) {
×
263
        return testUrl;
×
264
      }
265
      // WebLogic fix: check if the URL's file exists in the filesystem.
266
      if (log.isLoggable(Level.FINER)) {
×
267
        log.log(Level.FINER, "Not a JAR: " + jarUrl);
×
268
      }
269
      jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
×
NEW
270
      File file = Path.of(jarUrl.toString()).toFile();
×
271

272
      // File name might be URL-encoded
273
      if (!file.exists()) {
×
NEW
274
        file = Path.of(URLEncoder.encode(jarUrl.toString(), StandardCharsets.UTF_8)).toFile();
×
275
      }
276

277
      if (file.exists()) {
×
278
        if (log.isLoggable(Level.FINER)) {
×
279
          log.log(Level.FINER, "Trying real file: " + file.getAbsolutePath());
×
280
        }
281
        testUrl = file.toURI().toURL();
×
282
        if (isJar(testUrl)) {
×
283
          return testUrl;
×
284
        }
285
      }
286
    } catch (MalformedURLException e) {
×
287
      log.log(Level.WARNING, "Invalid JAR URL: " + jarUrl);
×
288
    }
×
289

290
    if (log.isLoggable(Level.FINER)) {
×
291
      log.log(Level.FINER, "Not a JAR: " + jarUrl);
×
292
    }
293
    return null;
×
294
  }
295

296
  /**
297
   * Converts a Java package name to a path that can be looked up with a call to
298
   * {@link ClassLoader#getResources(String)}.
299
   *
300
   * @param packageName
301
   *          The Java package name to convert to a path
302
   *
303
   * @return the package path
304
   */
305
  protected String getPackagePath(String packageName) {
306
    return packageName == null ? null : packageName.replace('.', '/');
×
307
  }
308

309
  /**
310
   * Returns true if the resource located at the given URL is a JAR file.
311
   *
312
   * @param url
313
   *          The URL of the resource to test.
314
   *
315
   * @return true, if is jar
316
   */
317
  protected boolean isJar(URL url) {
318
    return isJar(url, new byte[JAR_MAGIC.length]);
1✔
319
  }
320

321
  /**
322
   * Returns true if the resource located at the given URL is a JAR file.
323
   *
324
   * @param url
325
   *          The URL of the resource to test.
326
   * @param buffer
327
   *          A buffer into which the first few bytes of the resource are read. The buffer must be at least the size of
328
   *          {@link #JAR_MAGIC}. (The same buffer may be reused for multiple calls as an optimization.)
329
   *
330
   * @return true, if is jar
331
   */
332
  protected boolean isJar(URL url, byte[] buffer) {
333
    try (InputStream is = url.openStream()) {
1✔
334
      is.read(buffer, 0, JAR_MAGIC.length);
1✔
335
      if (Arrays.equals(buffer, JAR_MAGIC)) {
1!
336
        if (log.isLoggable(Level.FINER)) {
×
337
          log.log(Level.FINER, "Found JAR: " + url);
×
338
        }
339
        return true;
×
340
      }
341
    } catch (Exception e) {
×
342
      // Failure to read the stream means this is not a JAR
343
    }
1✔
344

345
    return false;
1✔
346
  }
347
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc