001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.common.io.resource;
019
020import com.google.common.collect.ImmutableSet;
021import com.google.common.io.ByteSource;
022import com.google.common.io.Resources;
023import com.google.common.net.MediaType;
024
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import java.io.IOException;
029import java.io.UncheckedIOException;
030import java.net.HttpURLConnection;
031import java.net.MalformedURLException;
032import java.net.URI;
033import java.net.URISyntaxException;
034import java.net.URL;
035import java.net.URLConnection;
036import java.nio.charset.Charset;
037
038/**
039 * Resource implemented with {@link URL#openStream()}.
040 *
041 * <p>Consider using {@link OkHttpResource} instead.
042 *
043 * <p>This also the base class of {@link ClasspathResource}.
044 */
045public class UrlResource extends BaseResource implements ReadableResource {
046
047    // TODO Remove, once there is a ClassLoaderResource
048
049    // TODO java.net.http <https://openjdk.org/groups/net/httpclient/intro.html> alternative!
050    //   Or not, as it has it's own problems? https://github.com/apache/maven-resolver/issues/739.
051
052    public enum Scheme {
053        jar,
054        http
055    }
056
057    public static class Provider implements ResourceProvider {
058
059        private final ImmutableSet<Scheme> schemes;
060
061        public Provider(Scheme... schemes) {
062            this.schemes = ImmutableSet.copyOf(schemes);
063        }
064
065        @Override
066        public Resource getResource(URI uri) {
067            var uriScheme = uri.getScheme();
068            for (var testScheme : schemes) {
069                if (uriScheme.startsWith(testScheme.name())) {
070                    try {
071                        return new ReadableButNotWritableDelegatingResource(
072                                new UrlResource(uri.toURL()));
073                    } catch (MalformedURLException e) {
074                        throw new IllegalArgumentException(
075                                "Malformed http: URI is not valid URL" + uri, e);
076                    }
077                }
078            }
079            return null;
080        }
081    }
082
083    private static final Logger LOG = LoggerFactory.getLogger(UrlResource.class);
084
085    private static final MediaTypeDetector mtd = new MediaTypeDetector();
086
087    private final URL url;
088
089    /**
090     * Constructor.
091     *
092     * @param uri URI of Resource; may be "logical", and e.g. include query parameters.
093     * @param url URL to read; must be "physical", and typically does not include query parameters.
094     * @param mediaType MediaType (incl. Charset)
095     */
096    public UrlResource(URI uri, URL url, MediaType mediaType) {
097        super(uri, mediaType);
098        this.url = url;
099    }
100
101    public UrlResource(URI uri, URL url) {
102        this(uri, url, mediaType(url));
103    }
104
105    public UrlResource(URL url) {
106        this(create(url), url, mediaType(url));
107    }
108
109    public UrlResource(URL url, MediaType mediaType) {
110        super(create(url), mediaType);
111        this.url = url;
112    }
113
114    private static URI create(URL url) {
115        try {
116            return url.toURI();
117        } catch (URISyntaxException e) {
118            throw new IllegalArgumentException("Invalid Syntax: " + url, e);
119        }
120    }
121
122    // See also OkHttpResource#mediaType(String url)
123    private static MediaType mediaType(URL url) {
124        // This is slow - but more correct; see https://www.baeldung.com/java-file-mime-type
125        URLConnection c = null;
126        try {
127            LOG.trace("mediaType: openConnection {}", url);
128            c = url.openConnection();
129            c.connect(); // MUST connect(), else failures are ignored!
130            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options ?
131            var contentTypeFromServer = c.getContentType();
132            var encodingFromServer = c.getContentEncoding();
133
134            var mediaTypeFromServer = MediaType.parse(contentTypeFromServer);
135            if (encodingFromServer != null) {
136                Charset charsetFromServer = Charset.forName(encodingFromServer);
137                mediaTypeFromServer = mediaTypeFromServer.withCharset(charsetFromServer);
138            }
139            return mtd.overwrite(url.toURI(), mediaTypeFromServer);
140
141        } catch (IOException e) {
142            throw new UncheckedIOException(e);
143        } catch (URISyntaxException e) {
144            throw new IllegalArgumentException(e.getMessage(), e);
145        } finally {
146            if (c instanceof HttpURLConnection) {
147                ((HttpURLConnection) c).disconnect();
148            }
149        }
150    }
151
152    @Override
153    public ByteSource byteSource() {
154        return Resources.asByteSource(url);
155    }
156}