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}