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}