Skip to content

Commit

Permalink
Merge pull request #1 from harawata/custom-vfs
Browse files Browse the repository at this point in the history
Demonstrating a fix using custom VFS implementation.
  • Loading branch information
kazuki43zoo committed Aug 18, 2015
2 parents 33371e9 + f0a5ec9 commit 5548e10
Show file tree
Hide file tree
Showing 3 changed files with 363 additions and 1 deletion.
2 changes: 1 addition & 1 deletion mybatis-typehandler/pom.xml
Expand Up @@ -12,7 +12,7 @@
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
<version>3.4.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
Expand Down
@@ -0,0 +1,361 @@
/**
* Copyright 2009-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.infra.mybatis.vfs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.ibatis.io.VFS;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

/**
* Modified DefaultVFS for handling nested jar.
*/
public class SpringBootExecutableJarVFS extends VFS {
private static final Log log = LogFactory.getLog(SpringBootExecutableJarVFS.class);

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

@Override
public boolean isValid() {
return true;
}

@Override
public List<String> list(URL url, String path) throws IOException {
InputStream is = null;
try {
List<String> resources = new ArrayList<String>();

// First, try to find the URL of a JAR file containing the requested resource. If a JAR
// file is found, then we'll list child resources by reading the JAR.
URL jarUrl = findJarForResource(url);
if (jarUrl != null) {
is = jarUrl.openStream();
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
resources = listResources(new JarInputStream(is), path);
}
else {
List<String> children = new ArrayList<String>();
try {
if (isJar(url)) {
// Some versions of JBoss VFS might give a JAR stream even if the resource
// referenced by the URL isn't actually a JAR
is = url.openStream();
JarInputStream jarInput = new JarInputStream(is);
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
if (log.isDebugEnabled()) {
log.debug("Jar entry: " + entry.getName());
}
children.add(entry.getName());
}
jarInput.close();
}
else {
/*
* Some servlet containers allow reading from directory resources like a
* text file, listing the child resources one per line. However, there is no
* way to differentiate between directory and file resources just by reading
* them. To work around that, as each line is read, try to look it up via
* the class loader as a child of the current resource. If any line fails
* then we assume the current resource is not a directory.
*/
is = url.openStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
List<String> lines = new ArrayList<String>();
for (String line; (line = reader.readLine()) != null;) {
if (log.isDebugEnabled()) {
log.debug("Reader entry: " + line);
}
lines.add(line);
if (getResources(path + "/" + line).isEmpty()) {
lines.clear();
break;
}
}

if (!lines.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
children.addAll(lines);
}
}
} catch (FileNotFoundException e) {
/*
* For file URLs the openStream() call might fail, depending on the servlet
* container, because directories can't be opened for reading. If that happens,
* then list the directory directly instead.
*/
if ("file".equals(url.getProtocol())) {
File file = new File(url.getFile());
if (log.isDebugEnabled()) {
log.debug("Listing directory " + file.getAbsolutePath());
}
if (file.isDirectory()) {
if (log.isDebugEnabled()) {
log.debug("Listing " + url);
}
children = Arrays.asList(file.list());
}
}
else {
// No idea where the exception came from so rethrow it
throw e;
}
}

// The URL prefix to use when recursively listing child resources
String prefix = url.toExternalForm();
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}

// Iterate over immediate children, adding files and recursing into directories
for (String child : children) {
String resourcePath = path + "/" + child;
resources.add(resourcePath);
URL childUrl = new URL(prefix + child);
resources.addAll(list(childUrl, resourcePath));
}
}

return resources;
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
// Ignore
}
}
}
}

/**
* List the names of the entries in the given {@link JarInputStream} that begin with the
* specified {@code path}. Entries will match with or without a leading slash.
*
* @param jar The JAR input stream
* @param path The leading path to match
* @return The names of all the matching entries
* @throws IOException If I/O errors occur
*/
protected List<String> listResources(JarInputStream jar, String path) throws IOException {
// Include the leading and trailing slash when matching names
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith("/")) {
path = path + "/";
}

// Iterate over the entries and collect those that begin with the requested path
List<String> resources = new ArrayList<String>();
for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
if (!entry.isDirectory()) {
// Add leading slash if it's missing
String name = entry.getName();
if (!name.startsWith("/")) {
name = "/" + name;
}

// Check file name
if (name.startsWith(path)) {
if (log.isDebugEnabled()) {
log.debug("Found resource: " + name);
}
// Trim leading slash
resources.add(name.substring(1));
}
}
}
return resources;
}

/**
* Attempts to deconstruct the given URL to find a JAR file containing the resource referenced
* by the URL. That is, assuming the URL references a JAR entry, this method will return a URL
* that references the JAR file containing the entry. If the JAR cannot be located, then this
* method returns null.
*
* @param url The URL of the JAR entry.
* @return The URL of the JAR file, if one is found. Null if not.
* @throws MalformedURLException
*/
protected URL findJarForResource(URL url) throws MalformedURLException {
if (log.isDebugEnabled()) {
log.debug("Find JAR URL: " + url);
}

if (isNestedJar(url)) {
// Retain jar: protocol as a workaround for #325
if (log.isDebugEnabled()) {
log.debug("It is a nested JAR: " + url);
}
} else {
// If the file part of the URL is itself a URL, then that URL probably points to the JAR
try {
for (;;) {
url = new URL(url.getFile());
if (log.isDebugEnabled()) {
log.debug("Inner URL: " + url);
}
}
} catch (MalformedURLException e) {
// This will happen at some point and serves as a break in the loop
}
}

// Look for the .jar extension and chop off everything after that
StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
int index = jarUrl.lastIndexOf(".jar");
if (index >= 0) {
jarUrl.setLength(index + 4);
if (log.isDebugEnabled()) {
log.debug("Extracted JAR URL: " + jarUrl);
}
}
else {
if (log.isDebugEnabled()) {
log.debug("Not a JAR: " + jarUrl);
}
return null;
}

// Try to open and test it
try {
URL testUrl = new URL(jarUrl.toString());
if (isJar(testUrl)) {
return testUrl;
}
else {
// WebLogic fix: check if the URL's file exists in the filesystem.
if (log.isDebugEnabled()) {
log.debug("Not a JAR: " + jarUrl);
}
jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
File file = new File(jarUrl.toString());

// File name might be URL-encoded
if (!file.exists()) {
try {
file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unsupported encoding? UTF-8? That's unpossible.");
}
}

if (file.exists()) {
if (log.isDebugEnabled()) {
log.debug("Trying real file: " + file.getAbsolutePath());
}
testUrl = file.toURI().toURL();
if (isJar(testUrl)) {
return testUrl;
}
}
}
} catch (MalformedURLException e) {
log.warn("Invalid JAR URL: " + jarUrl);
}

if (log.isDebugEnabled()) {
log.debug("Not a JAR: " + jarUrl);
}
return null;
}

protected boolean isNestedJar(URL url) {
if (!"jar".equals(url.getProtocol()))
return false;
String urlStr = url.toExternalForm();
return urlStr.indexOf(".jar!/") != urlStr.lastIndexOf(".jar!/");
}

/**
* Converts a Java package name to a path that can be looked up with a call to
* {@link ClassLoader#getResources(String)}.
*
* @param packageName The Java package name to convert to a path
*/
protected String getPackagePath(String packageName) {
return packageName == null ? null : packageName.replace('.', '/');
}

/**
* Returns true if the resource located at the given URL is a JAR file.
*
* @param url The URL of the resource to test.
*/
protected boolean isJar(URL url) {
return isJar(url, new byte[JAR_MAGIC.length]);
}

/**
* Returns true if the resource located at the given URL is a JAR file.
*
* @param url The URL of the resource to test.
* @param buffer A buffer into which the first few bytes of the resource are read. The buffer
* must be at least the size of {@link #JAR_MAGIC}. (The same buffer may be reused
* for multiple calls as an optimization.)
*/
protected boolean isJar(URL url, byte[] buffer) {
InputStream is = null;
try {
is = url.openStream();
is.read(buffer, 0, JAR_MAGIC.length);
if (Arrays.equals(buffer, JAR_MAGIC)) {
if (log.isDebugEnabled()) {
log.debug("Found JAR: " + url);
}
return true;
}
} catch (Exception e) {
// Failure to read the stream means this is not a JAR
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
// Ignore
}
}
}

return false;
}
}
1 change: 1 addition & 0 deletions spring-boot-app/src/main/resources/mybatis-config.xml
Expand Up @@ -7,6 +7,7 @@
<!-- See http://mybatis.github.io/mybatis-3/configuration.html#settings -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
<setting name="vfsImpl" value="sample.infra.mybatis.vfs.SpringBootExecutableJarVFS" />
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false" />
<!--
Expand Down

0 comments on commit 5548e10

Please sign in to comment.