001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.helm.chart; 018 019import java.io.BufferedInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022 023import java.net.URI; 024import java.net.URISyntaxException; 025import java.net.URL; 026import java.net.URLConnection; 027 028import java.nio.file.Files; 029import java.nio.file.Path; 030import java.nio.file.Paths; 031 032import java.util.IdentityHashMap; 033import java.util.Collection; 034import java.util.Iterator; 035import java.util.Map.Entry; 036import java.util.Objects; 037 038import java.util.zip.GZIPInputStream; 039import java.util.zip.ZipInputStream; 040 041import hapi.chart.ChartOuterClass.Chart; // for javadoc only 042 043import org.kamranzafar.jtar.TarInputStream; 044 045/** 046 * A {@link StreamOrientedChartLoader StreamOrientedChartLoader<URL>} that creates 047 * {@link Chart} instances from {@link URL} instances. 048 * 049 * <h2>Thread Safety</h2> 050 * 051 * <p>This class is safe for concurrent use by multiple threads.</p> 052 * 053 * @author <a href="https://about.me/lairdnelson" 054 * target="_parent">Laird Nelson</a> 055 * 056 * @see #toNamedInputStreamEntries(URL) 057 * 058 * @see StreamOrientedChartLoader 059 */ 060public class URLChartLoader extends StreamOrientedChartLoader<URL> { 061 062 063 /** 064 * Resources to be closed by the {@link #close()} method. 065 * 066 * <p>This field is never {@code null}.</p> 067 */ 068 private final IdentityHashMap<AutoCloseable, Void> closeables; 069 070 071 /* 072 * Constructors. 073 */ 074 075 076 /** 077 * Creates a new {@link URLChartLoader}. 078 */ 079 public URLChartLoader() { 080 super(); 081 this.closeables = new IdentityHashMap<>(); 082 } 083 084 085 /* 086 * Instance methods. 087 */ 088 089 090 /** 091 * Converts the supplied {@link URL} into an {@link Iterable} of 092 * {@link Entry} instances, each of which consists of an {@link 093 * InputStream} representing a resource within a Helm chart together 094 * with its (relative to the chart) name. 095 * 096 * <p>This method never returns {@code null}.</p> 097 * 098 * <p>Overrides of this method are not permitted to return {@code 099 * null}. 100 * 101 * @param url the {@link URL} to dereference; must be non-{@code 102 * null} or an effectively empty {@link Iterable} will be returned 103 * 104 * @return a non-{@code null} {@link Iterable} of {@link Entry} 105 * instances representing named {@link InputStream}s 106 * 107 * @exception IOException if there is a problem reading from the 108 * supplied {@link URL} 109 */ 110 @Override 111 protected Iterable<? extends Entry<? extends String, ? extends InputStream>> toNamedInputStreamEntries(final URL url) throws IOException { 112 Objects.requireNonNull(url); 113 final String scheme = url.getProtocol(); 114 Path path = null; 115 if ("file".equals(scheme)) { 116 URI uri = null; 117 try { 118 uri = url.toURI(); 119 } catch (final URISyntaxException wrapMe) { 120 throw new IllegalArgumentException(wrapMe.getMessage(), wrapMe); 121 } 122 assert uri != null; 123 try { 124 path = Paths.get(uri); 125 } catch (final IllegalArgumentException notAFile) { 126 path = null; 127 } 128 } 129 final Iterable<? extends Entry<? extends String, ? extends InputStream>> returnValue; 130 if (path == null || !Files.isDirectory(path)) { 131 final String urlString = url.toString(); 132 assert urlString != null; 133 if (urlString.endsWith(".zip") || urlString.endsWith(".jar")) { 134 final ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(this.openStream(url))); 135 this.closeables.put(zipInputStream, null); 136 final ZipInputStreamChartLoader loader = new ZipInputStreamChartLoader(); 137 this.closeables.put(loader, null); 138 returnValue = loader.toNamedInputStreamEntries(zipInputStream); 139 } else { 140 final TarInputStream tarInputStream = new TarInputStream(new GZIPInputStream(new BufferedInputStream(this.openStream(url)))); 141 this.closeables.put(tarInputStream, null); 142 final TapeArchiveChartLoader loader = new TapeArchiveChartLoader(); 143 this.closeables.put(loader, null); 144 returnValue = loader.toNamedInputStreamEntries(tarInputStream); 145 } 146 } else { 147 final DirectoryChartLoader loader = new DirectoryChartLoader(); 148 this.closeables.put(loader, null); 149 returnValue = loader.toNamedInputStreamEntries(path); 150 } 151 return returnValue; 152 } 153 154 /** 155 * Returns an {@link InputStream} corresponding to the supplied 156 * {@link URL}. 157 * 158 * <p>This method may return {@code null}.</p> 159 * 160 * <p>Overrides of this method are permitted to return {@code 161 * null}.</p> 162 * 163 * @param url the {@link URL} whose affiliated {@link InputStream} 164 * should be returned; may be {@code null} in which case {@code 165 * null} will be returned 166 * 167 * @return an {@link InputStream} appropriate for the supplied 168 * {@link URL}, or {@code null} 169 * 170 * @exception IOException if an error occurs while connecting to the 171 * supplied {@link URL} 172 */ 173 protected InputStream openStream(final URL url) throws IOException { 174 InputStream returnValue = null; 175 if (url != null) { 176 final URLConnection urlConnection = url.openConnection(); 177 assert urlConnection != null; 178 urlConnection.setRequestProperty("User-Agent", "microbean-helm"); 179 returnValue = urlConnection.getInputStream(); 180 } 181 return returnValue; 182 } 183 184 /** 185 * Closes resources opened by this {@link URLChartLoader}'s {@link 186 * #toNamedInputStreamEntries(URL)} method. 187 * 188 * @exception IOException if a subclass has overridden this method 189 * and an error occurs 190 */ 191 @Override 192 public void close() throws IOException { 193 if (!this.closeables.isEmpty()) { 194 final Collection<? extends AutoCloseable> keys = this.closeables.keySet(); 195 if (keys != null && !keys.isEmpty()) { 196 final Iterator<? extends AutoCloseable> iterator = keys.iterator(); 197 if (iterator != null) { 198 while (iterator.hasNext()) { 199 final AutoCloseable closeable = iterator.next(); 200 if (closeable != null) { 201 try { 202 closeable.close(); 203 } catch (final IOException | RuntimeException throwMe) { 204 throw throwMe; 205 } catch (final Exception willNeverHappen) { 206 throw new AssertionError(willNeverHappen); 207 } 208 } 209 iterator.remove(); 210 } 211 } 212 } 213 } 214 assert this.closeables.isEmpty(); 215 } 216 217}