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}