/* * 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 org.jdbi.v3.core.locator; import java.io.IOException; import java.io.InputStream; import java.util.AbstractMap; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.antlr.runtime.ANTLRInputStream; import org.jdbi.v3.core.internal.SqlScriptParser; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; /** * Locates SQL in <code>.sql</code> files on the classpath. */ public final class ClasspathSqlLocator { private static final char PACKAGE_DELIMITER = '.'; private static final char PATH_DELIMITER = '/'; private static final SqlScriptParser SQL_SCRIPT_PARSER = new SqlScriptParser((t, sb) -> sb.append(t.getText())); @SuppressWarnings("unchecked") private static final Map<Entry<ClassLoader, String>, String> CACHE = ExpiringMap.builder() .expiration(10, TimeUnit.MINUTES) .expirationPolicy(ExpirationPolicy.ACCESSED) .entryLoader(obj -> { Entry<ClassLoader, String> entry = (Entry<ClassLoader, String>) obj; return readResource(entry.getKey(), entry.getValue()); }) .build(); private static final String SQL_EXTENSION = ".sql"; private ClasspathSqlLocator() { } /** * Locates SQL for the given type and name. Example: Given a type <code>com.foo.Bar</code> and a name of * <code>baz</code>, looks for a resource named <code>com/foo/Bar/baz.sql</code> on the classpath and returns its * contents as a String. * * @param type the type that "owns" the given SQL. Dictates the directory path to the SQL resource file on the * classpath. * @param name the SQL statement name (usually a method or field name from the type). * @return the located SQL. */ public static String findSqlOnClasspath(Class<?> type, String name) { String path = resourcePathFor(type, name); return getResourceOnClasspath(type.getClassLoader(), path); } /** * Locates SQL for the given fully-qualified name. Example: Given the name <code>com.foo.Bar.baz</code>, looks for * a resource named <code>com/foo/Bar/baz.sql</code> on the classpath and returns its contents as a String. * * @param name fully qualified name. * @return the located SQL. */ public static String findSqlOnClasspath(String name) { String path = resourcePathFor(name); return getResourceOnClasspath(selectClassLoader(), path); } private static String resourcePathFor(Class<?> extensionType, String methodName) { return resourcePathFor(extensionType.getName() + "." + methodName); } private static String resourcePathFor(String fullyQualifiedName) { return fullyQualifiedName.replace(PACKAGE_DELIMITER, PATH_DELIMITER) + SQL_EXTENSION; } /** * Returns resource's contents as a string at the specified path. The path should point directly * to the resource at the classpath. The resource is loaded by the current thread's classloader. * * @param path the resource path * @return the resource's contents * @see ClassLoader#getResource(String) */ public static String getResourceOnClasspath(String path) { return getResourceOnClasspath(selectClassLoader(), path); } /** * Returns resource's contents as a string at the specified path by the specified classloader. * The path should point directly to the resource at the classpath. The classloader should have * access to the resource. * * @param classLoader the classloader which loads the resource * @param path the resource path * @return the resource's contents * @see ClassLoader#getResource(String) */ public static String getResourceOnClasspath(ClassLoader classLoader, String path) { return CACHE.get(new AbstractMap.SimpleEntry<>(classLoader, path)); } private static String readResource(ClassLoader classLoader, String path) { try (InputStream is = openStream(classLoader, path)) { // strips away comments return SQL_SCRIPT_PARSER.parse(new ANTLRInputStream(is)); } catch (IOException e) { throw new RuntimeException("Unable to read classpath resource at " + path, e); } } private static InputStream openStream(ClassLoader classLoader, String path) { InputStream is = classLoader.getResourceAsStream(path); if (is == null) { throw new IllegalArgumentException("Cannot find classpath resource at " + path); } return is; } private static ClassLoader selectClassLoader() { return Optional.ofNullable(Thread.currentThread().getContextClassLoader()) .orElseGet(ClasspathSqlLocator.class::getClassLoader); } }