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}