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