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}