001/* 002 * Copyright 2023 the original author or authors. 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.tools.io; 017 018import static de.cuioss.tools.base.Preconditions.checkState; 019import static de.cuioss.tools.string.MoreStrings.isEmpty; 020import static de.cuioss.tools.string.MoreStrings.requireNotEmpty; 021import static java.util.Objects.requireNonNull; 022 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.Serial; 026import java.net.URL; 027import java.util.Optional; 028 029import de.cuioss.tools.logging.CuiLogger; 030import lombok.EqualsAndHashCode; 031import lombok.Getter; 032import lombok.ToString; 033 034/** 035 * Variant of {@link FileLoader} that loads files from the classpath. 036 * 037 * @author Oliver Wolff 038 */ 039@EqualsAndHashCode(of = { "normalizedPathName" }) 040@ToString 041public class ClassPathLoader implements FileLoader { 042 043 @Serial 044 private static final long serialVersionUID = 9140071059594577808L; 045 046 private static final CuiLogger log = new CuiLogger(ClassPathLoader.class); 047 048 private final String normalizedPathName; 049 050 private final String givenPathName; 051 052 @Getter 053 private final StructuredFilename fileName; 054 055 private URL url; 056 057 /** 058 * @param pathName must not be null nor empty, may start with the prefix 059 * {@link FileTypePrefix#CLASSPATH} but not with 060 * {@link FileTypePrefix#FILE} and contain at least one 061 * character despite the prefix. On all other cases a 062 * {@link IllegalArgumentException} will be thrown. 063 */ 064 public ClassPathLoader(final String pathName) { 065 requireNonNull(pathName); 066 givenPathName = pathName; 067 normalizedPathName = checkClasspathName(pathName); 068 fileName = new StructuredFilename(FilenameUtils.getName(normalizedPathName)); 069 } 070 071 /** 072 * Checks and modifies a given pathName 073 * 074 * @param pathName must not be null nor empty, may start with the prefix 075 * {@link FileTypePrefix#CLASSPATH} but not with 076 * {@link FileTypePrefix#FILE} and contain at least one 077 * character despite the prefix. On all other cases a 078 * {@link IllegalArgumentException} will be thrown. 079 * @return the normalized pathname without prefix but with a leading '/' 080 */ 081 static String checkClasspathName(final String pathName) { 082 requireNotEmpty(pathName); 083 if (FileTypePrefix.FILE.is(pathName)) { 084 throw new IllegalArgumentException( 085 "Invalid path name, must start not start with " + FileTypePrefix.FILE + " but was: " + pathName); 086 } 087 var newPathName = pathName; 088 if (FileTypePrefix.CLASSPATH.is(pathName)) { 089 newPathName = FileTypePrefix.CLASSPATH.removePrefix(pathName); 090 } 091 092 if (isEmpty(newPathName)) { 093 throw new IllegalArgumentException("Filename " + pathName + " is invalid"); 094 } 095 if (newPathName.indexOf('/') != 0) { 096 newPathName = '/' + newPathName; 097 } 098 return newPathName; 099 } 100 101 @Override 102 public boolean isReadable() { 103 return null != getURL(); 104 } 105 106 @Override 107 public InputStream inputStream() { 108 checkState(isReadable(), "Resource '%s' is not readable", givenPathName); 109 try { 110 return getURL().openStream(); 111 } catch (IOException e) { 112 throw new IllegalStateException("Unable to load classpath file for " + givenPathName, e); 113 } 114 } 115 116 @Override 117 public boolean isFilesystemLoader() { 118 return false; 119 } 120 121 @Override 122 public URL getURL() { 123 if (null == url) { 124 url = resolveUrl(normalizedPathName); 125 } 126 return url; 127 } 128 129 private static URL resolveUrl(String path) { 130 log.debug("Resolving URL for '{}'", path); 131 var url = ClassPathLoader.class.getResource(path); 132 if (null != url) { 133 log.debug("Resolved '{}' from ClassPathLoader.class", path); 134 return url; 135 } 136 var loader = Optional.ofNullable(Thread.currentThread().getContextClassLoader()); 137 if (loader.isPresent()) { 138 url = loader.get().getResource(path); 139 if (null != url) { 140 log.debug("Resolved '{}' from ContextClassLoader", path); 141 return url; 142 } 143 } 144 log.warn("Unable to resolve '{}' from classpath", path); 145 return null; 146 } 147 148}