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.nio.file.Files;
024import java.nio.file.LinkOption; // for javadoc only
025import java.nio.file.Path;
026
027import java.util.AbstractMap.SimpleImmutableEntry;
028import java.util.Iterator;
029import java.util.Map.Entry;
030import java.util.NoSuchElementException;
031import java.util.Objects;
032
033import java.util.stream.Stream;
034
035import hapi.chart.ChartOuterClass.Chart; // for javadoc only
036
037/**
038 * A {@link StreamOrientedChartLoader
039 * StreamOrientedChartLoader<Path>} that creates {@link Chart}
040 * instances from filesystem directories represented as {@link Path}
041 * objects.
042 *
043 * @author <a href="https://about.me/lairdnelson"
044 * target="_parent">Laird Nelson</a>
045 *
046 * @see #toNamedInputStreamEntries(Path)
047 *
048 * @see StreamOrientedChartLoader
049 */
050public class DirectoryChartLoader extends StreamOrientedChartLoader<Path> {
051
052
053  /*
054   * Constructors.
055   */
056  
057  
058  /**
059   * Creates a new {@link DirectoryChartLoader}.
060   */
061  public DirectoryChartLoader() {
062    super();
063  }
064
065
066  /*
067   * Instance methods.
068   */
069  
070
071  /**
072   * Converts the supplied {@link Path}, which must be non-{@code
073   * null} and {@linkplain Files#isDirectory(Path, LinkOption...) a
074   * directory}, into an {@link Iterable} of {@link Entry} instances,
075   * each of which consists of an {@link InputStream} associated with
076   * a name.
077   *
078   * <p>This method never returns {@code null}.</p>
079   *
080   * <p>Overrides of this method are not permitted to return {@code
081   * null}.
082   *
083   * @param path the {@link Path} to read; must be non-{@code null}
084   * and must be {@linkplain Files#isDirectory(Path, LinkOption...) a
085   * directory} or an effectively empty {@link Iterable} will be
086   * returned
087   *
088   * @return a non-{@code null} {@link Iterable} of {@link Entry}
089   * instances representing named {@link InputStream}s
090   *
091   * @exception IOException if there is a problem reading from the
092   * directory represented by the supplied {@link Path} or any of its
093   * subdirectories or files
094   */
095  @Override
096  protected Iterable<? extends Entry<? extends String, ? extends InputStream>> toNamedInputStreamEntries(final Path path) throws IOException {
097    final Iterable<Entry<String, InputStream>> returnValue;
098    if (path == null || !Files.isDirectory(path)) {
099      returnValue = new EmptyIterable();
100    } else {
101      returnValue = new PathWalker(path);
102    }
103    return returnValue;
104  }
105
106
107  /*
108   * Inner and nested classes.
109   */
110
111  
112  private static final class PathWalker implements Iterable<Entry<String, InputStream>> {
113
114    private final Path directoryParent;
115
116    private final Stream<? extends Path> pathStream;
117    
118    private PathWalker(final Path directory) throws IOException {
119      super();
120      Objects.requireNonNull(directory);
121      if (!Files.isDirectory(directory)) {
122        throw new IllegalArgumentException("!Files.isDirectory(directory): " + directory);
123      }
124      final Path directoryParent = directory.getParent();
125      if (directoryParent == null) {
126        throw new IllegalArgumentException("directory.getParent() == null");
127      }
128      this.directoryParent = directoryParent;
129      final Stream<Path> pathStream;
130      final Path helmIgnore = directory.resolve(".helmIgnore");
131      assert helmIgnore != null;
132      // TODO: p in the filters below needs to be tested to see if
133      // it's, for example, foo/charts/bar/.fred--that .-prefixed
134      // directory and all of its files has to be ignored.
135      if (!Files.exists(helmIgnore)) {
136        pathStream = Files.walk(directory)
137          .filter(p -> p != null && !Files.isDirectory(p));
138      } else {
139        final HelmIgnorePathMatcher helmIgnorePathMatcher = new HelmIgnorePathMatcher(helmIgnore);
140        pathStream = Files.walk(directory)
141          .filter(p -> p != null && !Files.isDirectory(p) && !helmIgnorePathMatcher.matches(p));
142      }
143      this.pathStream = pathStream;
144    }
145
146    @Override
147    public final Iterator<Entry<String, InputStream>> iterator() {
148      return new PathIterator(this.directoryParent, this.pathStream.iterator());
149    }
150    
151  }
152
153  private static final class PathIterator implements Iterator<Entry<String, InputStream>> {
154
155    private final Path directoryParent;
156    
157    private final Iterator<? extends Path> pathIterator;
158
159    private Entry<String, InputStream> currentEntry;
160    
161    private PathIterator(final Path directoryParent, final Iterator<? extends Path> pathIterator) {
162      super();
163      Objects.requireNonNull(directoryParent);
164      Objects.requireNonNull(pathIterator);
165      if (!Files.isDirectory(directoryParent)) {
166        throw new IllegalArgumentException("!Files.isDirectory(directoryParent): " + directoryParent);
167      }
168      this.directoryParent = directoryParent;
169      this.pathIterator = pathIterator;
170    }
171
172    @Override
173    public final boolean hasNext() {
174      if (this.currentEntry != null) {
175        final InputStream oldStream = this.currentEntry.getValue();
176        if (oldStream != null) {
177          try {
178            oldStream.close();
179          } catch (final IOException ignore) {
180
181          }
182        }
183        this.currentEntry = null;
184      }      
185      return this.pathIterator != null && this.pathIterator.hasNext();
186    }
187
188    @Override
189    public final Entry<String, InputStream> next() {
190      final Path originalFile = this.pathIterator.next();
191      assert originalFile != null;
192      assert !Files.isDirectory(originalFile);
193      final Path relativeFile = this.directoryParent.relativize(originalFile);
194      assert relativeFile != null;
195      final String relativePathString = relativeFile.toString().replace('\\', '/');
196      assert relativePathString != null;
197      try {
198        this.currentEntry = new SimpleImmutableEntry<>(relativePathString, new BufferedInputStream(Files.newInputStream(originalFile)));
199      } catch (final IOException wrapMe) {
200        throw (NoSuchElementException)new NoSuchElementException(wrapMe.getMessage()).initCause(wrapMe);
201      }
202      return this.currentEntry;
203    }
204    
205  }
206
207  /**
208   * Does nothing on purpose.
209   *
210   * @exception IOException if a subclass has overridden this method
211   * and an error occurs
212   */
213  public void close() throws IOException {
214    
215  }
216  
217}