001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017 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; 026 027import java.nio.file.Files; 028import java.nio.file.Path; 029import java.nio.file.Paths; 030 031import java.util.IdentityHashMap; 032import java.util.Collection; 033import java.util.Iterator; 034import java.util.Map.Entry; 035import java.util.Objects; 036 037import java.util.zip.GZIPInputStream; 038import java.util.zip.ZipInputStream; 039 040import hapi.chart.ChartOuterClass.Chart; // for javadoc only 041 042import org.kamranzafar.jtar.TarInputStream; 043 044/** 045 * A {@link StreamOrientedChartLoader StreamOrientedChartLoader<URL>} that creates 046 * {@link Chart} instances from {@link URL} instances. 047 * 048 * <h2>Thread Safety</h2> 049 * 050 * <p>This class is safe for concurrent use by multiple threads.</p> 051 * 052 * @author <a href="https://about.me/lairdnelson" 053 * target="_parent">Laird Nelson</a> 054 * 055 * @see #toNamedInputStreamEntries(URL) 056 * 057 * @see StreamOrientedChartLoader 058 */ 059public class URLChartLoader extends StreamOrientedChartLoader<URL> { 060 061 062 /** 063 * Resources to be closed by the {@link #close()} method. 064 * 065 * <p>This field is never {@code null}.</p> 066 */ 067 private final IdentityHashMap<AutoCloseable, Void> closeables; 068 069 070 /* 071 * Constructors. 072 */ 073 074 075 /** 076 * Creates a new {@link URLChartLoader}. 077 */ 078 public URLChartLoader() { 079 super(); 080 this.closeables = new IdentityHashMap<>(); 081 } 082 083 084 /* 085 * Instance methods. 086 */ 087 088 089 /** 090 * Converts the supplied {@link URL} into an {@link Iterable} of 091 * {@link Entry} instances, each of which consists of an {@link 092 * InputStream} representing a resource within a Helm chart together 093 * with its (relative to the chart) name. 094 * 095 * <p>This method never returns {@code null}.</p> 096 * 097 * <p>Overrides of this method are not permitted to return {@code 098 * null}. 099 * 100 * @param url the {@link URL} to dereference; must be non-{@code 101 * null} or an effectively empty {@link Iterable} will be returned 102 * 103 * @return a non-{@code null} {@link Iterable} of {@link Entry} 104 * instances representing named {@link InputStream}s 105 * 106 * @exception IOException if there is a problem reading from the 107 * supplied {@link URL} 108 */ 109 @Override 110 protected Iterable<? extends Entry<? extends String, ? extends InputStream>> toNamedInputStreamEntries(final URL url) throws IOException { 111 Objects.requireNonNull(url); 112 final String scheme = url.getProtocol(); 113 Path path = null; 114 if ("file".equals(scheme)) { 115 URI uri = null; 116 try { 117 uri = url.toURI(); 118 } catch (final URISyntaxException wrapMe) { 119 throw new IllegalArgumentException(wrapMe.getMessage(), wrapMe); 120 } 121 assert uri != null; 122 try { 123 path = Paths.get(uri); 124 } catch (final IllegalArgumentException notAFile) { 125 path = null; 126 } 127 } 128 final Iterable<? extends Entry<? extends String, ? extends InputStream>> returnValue; 129 if (path == null || !Files.isDirectory(path)) { 130 final String urlString = url.toString(); 131 assert urlString != null; 132 if (urlString.endsWith(".zip") || urlString.endsWith(".jar")) { 133 final ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(url.openStream())); 134 this.closeables.put(zipInputStream, null); 135 final ZipInputStreamChartLoader loader = new ZipInputStreamChartLoader(); 136 this.closeables.put(loader, null); 137 returnValue = loader.toNamedInputStreamEntries(zipInputStream); 138 } else { 139 final TarInputStream tarInputStream = new TarInputStream(new GZIPInputStream(new BufferedInputStream(url.openStream()))); 140 this.closeables.put(tarInputStream, null); 141 final TapeArchiveChartLoader loader = new TapeArchiveChartLoader(); 142 this.closeables.put(loader, null); 143 returnValue = loader.toNamedInputStreamEntries(tarInputStream); 144 } 145 } else { 146 final DirectoryChartLoader loader = new DirectoryChartLoader(); 147 this.closeables.put(loader, null); 148 returnValue = loader.toNamedInputStreamEntries(path); 149 } 150 return returnValue; 151 } 152 153 /** 154 * Closes resources opened by this {@link URLChartLoader}'s {@link 155 * #toNamedInputStreamEntries(URL)} method. 156 * 157 * @exception IOException if a subclass has overridden this method 158 * and an error occurs 159 */ 160 @Override 161 public void close() throws IOException { 162 if (!this.closeables.isEmpty()) { 163 final Collection<? extends AutoCloseable> keys = this.closeables.keySet(); 164 if (keys != null && !keys.isEmpty()) { 165 final Iterator<? extends AutoCloseable> iterator = keys.iterator(); 166 if (iterator != null) { 167 while (iterator.hasNext()) { 168 final AutoCloseable closeable = iterator.next(); 169 if (closeable != null) { 170 try { 171 closeable.close(); 172 } catch (final IOException | RuntimeException throwMe) { 173 throw throwMe; 174 } catch (final Exception willNeverHappen) { 175 throw new AssertionError(willNeverHappen); 176 } 177 } 178 iterator.remove(); 179 } 180 } 181 } 182 } 183 assert this.closeables.isEmpty(); 184 } 185 186}