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}