001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019 package org.apache.hadoop.mapreduce.lib.input; 020 021 import java.io.IOException; 022 import java.util.ArrayList; 023 import java.util.Collection; 024 import java.util.LinkedList; 025 import java.util.HashSet; 026 import java.util.List; 027 import java.util.HashMap; 028 import java.util.Set; 029 import java.util.Iterator; 030 import java.util.Map; 031 032 import org.apache.hadoop.classification.InterfaceAudience; 033 import org.apache.hadoop.classification.InterfaceStability; 034 import org.apache.hadoop.conf.Configuration; 035 import org.apache.hadoop.fs.FileSystem; 036 import org.apache.hadoop.fs.FileUtil; 037 import org.apache.hadoop.fs.Path; 038 import org.apache.hadoop.fs.BlockLocation; 039 import org.apache.hadoop.fs.FileStatus; 040 import org.apache.hadoop.fs.PathFilter; 041 import org.apache.hadoop.io.compress.CompressionCodec; 042 import org.apache.hadoop.io.compress.CompressionCodecFactory; 043 import org.apache.hadoop.io.compress.SplittableCompressionCodec; 044 import org.apache.hadoop.mapreduce.InputFormat; 045 import org.apache.hadoop.mapreduce.InputSplit; 046 import org.apache.hadoop.mapreduce.JobContext; 047 import org.apache.hadoop.mapreduce.RecordReader; 048 import org.apache.hadoop.mapreduce.TaskAttemptContext; 049 import org.apache.hadoop.net.NodeBase; 050 import org.apache.hadoop.net.NetworkTopology; 051 052 /** 053 * An abstract {@link InputFormat} that returns {@link CombineFileSplit}'s in 054 * {@link InputFormat#getSplits(JobContext)} method. 055 * 056 * Splits are constructed from the files under the input paths. 057 * A split cannot have files from different pools. 058 * Each split returned may contain blocks from different files. 059 * If a maxSplitSize is specified, then blocks on the same node are 060 * combined to form a single split. Blocks that are left over are 061 * then combined with other blocks in the same rack. 062 * If maxSplitSize is not specified, then blocks from the same rack 063 * are combined in a single split; no attempt is made to create 064 * node-local splits. 065 * If the maxSplitSize is equal to the block size, then this class 066 * is similar to the default splitting behavior in Hadoop: each 067 * block is a locally processed split. 068 * Subclasses implement 069 * {@link InputFormat#createRecordReader(InputSplit, TaskAttemptContext)} 070 * to construct <code>RecordReader</code>'s for 071 * <code>CombineFileSplit</code>'s. 072 * 073 * @see CombineFileSplit 074 */ 075 @InterfaceAudience.Public 076 @InterfaceStability.Stable 077 public abstract class CombineFileInputFormat<K, V> 078 extends FileInputFormat<K, V> { 079 080 public static final String SPLIT_MINSIZE_PERNODE = 081 "mapreduce.input.fileinputformat.split.minsize.per.node"; 082 public static final String SPLIT_MINSIZE_PERRACK = 083 "mapreduce.input.fileinputformat.split.minsize.per.rack"; 084 // ability to limit the size of a single split 085 private long maxSplitSize = 0; 086 private long minSplitSizeNode = 0; 087 private long minSplitSizeRack = 0; 088 089 // A pool of input paths filters. A split cannot have blocks from files 090 // across multiple pools. 091 private ArrayList<MultiPathFilter> pools = new ArrayList<MultiPathFilter>(); 092 093 // mapping from a rack name to the set of Nodes in the rack 094 private HashMap<String, Set<String>> rackToNodes = 095 new HashMap<String, Set<String>>(); 096 /** 097 * Specify the maximum size (in bytes) of each split. Each split is 098 * approximately equal to the specified size. 099 */ 100 protected void setMaxSplitSize(long maxSplitSize) { 101 this.maxSplitSize = maxSplitSize; 102 } 103 104 /** 105 * Specify the minimum size (in bytes) of each split per node. 106 * This applies to data that is left over after combining data on a single 107 * node into splits that are of maximum size specified by maxSplitSize. 108 * This leftover data will be combined into its own split if its size 109 * exceeds minSplitSizeNode. 110 */ 111 protected void setMinSplitSizeNode(long minSplitSizeNode) { 112 this.minSplitSizeNode = minSplitSizeNode; 113 } 114 115 /** 116 * Specify the minimum size (in bytes) of each split per rack. 117 * This applies to data that is left over after combining data on a single 118 * rack into splits that are of maximum size specified by maxSplitSize. 119 * This leftover data will be combined into its own split if its size 120 * exceeds minSplitSizeRack. 121 */ 122 protected void setMinSplitSizeRack(long minSplitSizeRack) { 123 this.minSplitSizeRack = minSplitSizeRack; 124 } 125 126 /** 127 * Create a new pool and add the filters to it. 128 * A split cannot have files from different pools. 129 */ 130 protected void createPool(List<PathFilter> filters) { 131 pools.add(new MultiPathFilter(filters)); 132 } 133 134 /** 135 * Create a new pool and add the filters to it. 136 * A pathname can satisfy any one of the specified filters. 137 * A split cannot have files from different pools. 138 */ 139 protected void createPool(PathFilter... filters) { 140 MultiPathFilter multi = new MultiPathFilter(); 141 for (PathFilter f: filters) { 142 multi.add(f); 143 } 144 pools.add(multi); 145 } 146 147 @Override 148 protected boolean isSplitable(JobContext context, Path file) { 149 final CompressionCodec codec = 150 new CompressionCodecFactory(context.getConfiguration()).getCodec(file); 151 if (null == codec) { 152 return true; 153 } 154 return codec instanceof SplittableCompressionCodec; 155 } 156 157 /** 158 * default constructor 159 */ 160 public CombineFileInputFormat() { 161 } 162 163 @Override 164 public List<InputSplit> getSplits(JobContext job) 165 throws IOException { 166 167 long minSizeNode = 0; 168 long minSizeRack = 0; 169 long maxSize = 0; 170 Configuration conf = job.getConfiguration(); 171 172 // the values specified by setxxxSplitSize() takes precedence over the 173 // values that might have been specified in the config 174 if (minSplitSizeNode != 0) { 175 minSizeNode = minSplitSizeNode; 176 } else { 177 minSizeNode = conf.getLong(SPLIT_MINSIZE_PERNODE, 0); 178 } 179 if (minSplitSizeRack != 0) { 180 minSizeRack = minSplitSizeRack; 181 } else { 182 minSizeRack = conf.getLong(SPLIT_MINSIZE_PERRACK, 0); 183 } 184 if (maxSplitSize != 0) { 185 maxSize = maxSplitSize; 186 } else { 187 maxSize = conf.getLong("mapreduce.input.fileinputformat.split.maxsize", 0); 188 } 189 if (minSizeNode != 0 && maxSize != 0 && minSizeNode > maxSize) { 190 throw new IOException("Minimum split size pernode " + minSizeNode + 191 " cannot be larger than maximum split size " + 192 maxSize); 193 } 194 if (minSizeRack != 0 && maxSize != 0 && minSizeRack > maxSize) { 195 throw new IOException("Minimum split size per rack" + minSizeRack + 196 " cannot be larger than maximum split size " + 197 maxSize); 198 } 199 if (minSizeRack != 0 && minSizeNode > minSizeRack) { 200 throw new IOException("Minimum split size per node" + minSizeNode + 201 " cannot be smaller than minimum split " + 202 "size per rack " + minSizeRack); 203 } 204 205 // all the files in input set 206 Path[] paths = FileUtil.stat2Paths( 207 listStatus(job).toArray(new FileStatus[0])); 208 List<InputSplit> splits = new ArrayList<InputSplit>(); 209 if (paths.length == 0) { 210 return splits; 211 } 212 213 // Convert them to Paths first. This is a costly operation and 214 // we should do it first, otherwise we will incur doing it multiple 215 // times, one time each for each pool in the next loop. 216 List<Path> newpaths = new LinkedList<Path>(); 217 for (int i = 0; i < paths.length; i++) { 218 FileSystem fs = paths[i].getFileSystem(conf); 219 Path p = fs.makeQualified(paths[i]); 220 newpaths.add(p); 221 } 222 223 // In one single iteration, process all the paths in a single pool. 224 // Processing one pool at a time ensures that a split contains paths 225 // from a single pool only. 226 for (MultiPathFilter onepool : pools) { 227 ArrayList<Path> myPaths = new ArrayList<Path>(); 228 229 // pick one input path. If it matches all the filters in a pool, 230 // add it to the output set 231 for (Iterator<Path> iter = newpaths.iterator(); iter.hasNext();) { 232 Path p = iter.next(); 233 if (onepool.accept(p)) { 234 myPaths.add(p); // add it to my output set 235 iter.remove(); 236 } 237 } 238 // create splits for all files in this pool. 239 getMoreSplits(job, myPaths.toArray(new Path[myPaths.size()]), 240 maxSize, minSizeNode, minSizeRack, splits); 241 } 242 243 // create splits for all files that are not in any pool. 244 getMoreSplits(job, newpaths.toArray(new Path[newpaths.size()]), 245 maxSize, minSizeNode, minSizeRack, splits); 246 247 // free up rackToNodes map 248 rackToNodes.clear(); 249 return splits; 250 } 251 252 /** 253 * Return all the splits in the specified set of paths 254 */ 255 private void getMoreSplits(JobContext job, Path[] paths, 256 long maxSize, long minSizeNode, long minSizeRack, 257 List<InputSplit> splits) 258 throws IOException { 259 Configuration conf = job.getConfiguration(); 260 261 // all blocks for all the files in input set 262 OneFileInfo[] files; 263 264 // mapping from a rack name to the list of blocks it has 265 HashMap<String, List<OneBlockInfo>> rackToBlocks = 266 new HashMap<String, List<OneBlockInfo>>(); 267 268 // mapping from a block to the nodes on which it has replicas 269 HashMap<OneBlockInfo, String[]> blockToNodes = 270 new HashMap<OneBlockInfo, String[]>(); 271 272 // mapping from a node to the list of blocks that it contains 273 HashMap<String, List<OneBlockInfo>> nodeToBlocks = 274 new HashMap<String, List<OneBlockInfo>>(); 275 276 files = new OneFileInfo[paths.length]; 277 if (paths.length == 0) { 278 return; 279 } 280 281 // populate all the blocks for all files 282 long totLength = 0; 283 for (int i = 0; i < paths.length; i++) { 284 files[i] = new OneFileInfo(paths[i], conf, isSplitable(job, paths[i]), 285 rackToBlocks, blockToNodes, nodeToBlocks, 286 rackToNodes, maxSize); 287 totLength += files[i].getLength(); 288 } 289 290 ArrayList<OneBlockInfo> validBlocks = new ArrayList<OneBlockInfo>(); 291 Set<String> nodes = new HashSet<String>(); 292 long curSplitSize = 0; 293 294 // process all nodes and create splits that are local 295 // to a node. 296 for (Iterator<Map.Entry<String, 297 List<OneBlockInfo>>> iter = nodeToBlocks.entrySet().iterator(); 298 iter.hasNext();) { 299 300 Map.Entry<String, List<OneBlockInfo>> one = iter.next(); 301 nodes.add(one.getKey()); 302 List<OneBlockInfo> blocksInNode = one.getValue(); 303 304 // for each block, copy it into validBlocks. Delete it from 305 // blockToNodes so that the same block does not appear in 306 // two different splits. 307 for (OneBlockInfo oneblock : blocksInNode) { 308 if (blockToNodes.containsKey(oneblock)) { 309 validBlocks.add(oneblock); 310 blockToNodes.remove(oneblock); 311 curSplitSize += oneblock.length; 312 313 // if the accumulated split size exceeds the maximum, then 314 // create this split. 315 if (maxSize != 0 && curSplitSize >= maxSize) { 316 // create an input split and add it to the splits array 317 addCreatedSplit(splits, nodes, validBlocks); 318 curSplitSize = 0; 319 validBlocks.clear(); 320 } 321 } 322 } 323 // if there were any blocks left over and their combined size is 324 // larger than minSplitNode, then combine them into one split. 325 // Otherwise add them back to the unprocessed pool. It is likely 326 // that they will be combined with other blocks from the 327 // same rack later on. 328 if (minSizeNode != 0 && curSplitSize >= minSizeNode) { 329 // create an input split and add it to the splits array 330 addCreatedSplit(splits, nodes, validBlocks); 331 } else { 332 for (OneBlockInfo oneblock : validBlocks) { 333 blockToNodes.put(oneblock, oneblock.hosts); 334 } 335 } 336 validBlocks.clear(); 337 nodes.clear(); 338 curSplitSize = 0; 339 } 340 341 // if blocks in a rack are below the specified minimum size, then keep them 342 // in 'overflow'. After the processing of all racks is complete, these 343 // overflow blocks will be combined into splits. 344 ArrayList<OneBlockInfo> overflowBlocks = new ArrayList<OneBlockInfo>(); 345 Set<String> racks = new HashSet<String>(); 346 347 // Process all racks over and over again until there is no more work to do. 348 while (blockToNodes.size() > 0) { 349 350 // Create one split for this rack before moving over to the next rack. 351 // Come back to this rack after creating a single split for each of the 352 // remaining racks. 353 // Process one rack location at a time, Combine all possible blocks that 354 // reside on this rack as one split. (constrained by minimum and maximum 355 // split size). 356 357 // iterate over all racks 358 for (Iterator<Map.Entry<String, List<OneBlockInfo>>> iter = 359 rackToBlocks.entrySet().iterator(); iter.hasNext();) { 360 361 Map.Entry<String, List<OneBlockInfo>> one = iter.next(); 362 racks.add(one.getKey()); 363 List<OneBlockInfo> blocks = one.getValue(); 364 365 // for each block, copy it into validBlocks. Delete it from 366 // blockToNodes so that the same block does not appear in 367 // two different splits. 368 boolean createdSplit = false; 369 for (OneBlockInfo oneblock : blocks) { 370 if (blockToNodes.containsKey(oneblock)) { 371 validBlocks.add(oneblock); 372 blockToNodes.remove(oneblock); 373 curSplitSize += oneblock.length; 374 375 // if the accumulated split size exceeds the maximum, then 376 // create this split. 377 if (maxSize != 0 && curSplitSize >= maxSize) { 378 // create an input split and add it to the splits array 379 addCreatedSplit(splits, getHosts(racks), validBlocks); 380 createdSplit = true; 381 break; 382 } 383 } 384 } 385 386 // if we created a split, then just go to the next rack 387 if (createdSplit) { 388 curSplitSize = 0; 389 validBlocks.clear(); 390 racks.clear(); 391 continue; 392 } 393 394 if (!validBlocks.isEmpty()) { 395 if (minSizeRack != 0 && curSplitSize >= minSizeRack) { 396 // if there is a minimum size specified, then create a single split 397 // otherwise, store these blocks into overflow data structure 398 addCreatedSplit(splits, getHosts(racks), validBlocks); 399 } else { 400 // There were a few blocks in this rack that 401 // remained to be processed. Keep them in 'overflow' block list. 402 // These will be combined later. 403 overflowBlocks.addAll(validBlocks); 404 } 405 } 406 curSplitSize = 0; 407 validBlocks.clear(); 408 racks.clear(); 409 } 410 } 411 412 assert blockToNodes.isEmpty(); 413 assert curSplitSize == 0; 414 assert validBlocks.isEmpty(); 415 assert racks.isEmpty(); 416 417 // Process all overflow blocks 418 for (OneBlockInfo oneblock : overflowBlocks) { 419 validBlocks.add(oneblock); 420 curSplitSize += oneblock.length; 421 422 // This might cause an exiting rack location to be re-added, 423 // but it should be ok. 424 for (int i = 0; i < oneblock.racks.length; i++) { 425 racks.add(oneblock.racks[i]); 426 } 427 428 // if the accumulated split size exceeds the maximum, then 429 // create this split. 430 if (maxSize != 0 && curSplitSize >= maxSize) { 431 // create an input split and add it to the splits array 432 addCreatedSplit(splits, getHosts(racks), validBlocks); 433 curSplitSize = 0; 434 validBlocks.clear(); 435 racks.clear(); 436 } 437 } 438 439 // Process any remaining blocks, if any. 440 if (!validBlocks.isEmpty()) { 441 addCreatedSplit(splits, getHosts(racks), validBlocks); 442 } 443 } 444 445 /** 446 * Create a single split from the list of blocks specified in validBlocks 447 * Add this new split into splitList. 448 */ 449 private void addCreatedSplit(List<InputSplit> splitList, 450 Collection<String> locations, 451 ArrayList<OneBlockInfo> validBlocks) { 452 // create an input split 453 Path[] fl = new Path[validBlocks.size()]; 454 long[] offset = new long[validBlocks.size()]; 455 long[] length = new long[validBlocks.size()]; 456 for (int i = 0; i < validBlocks.size(); i++) { 457 fl[i] = validBlocks.get(i).onepath; 458 offset[i] = validBlocks.get(i).offset; 459 length[i] = validBlocks.get(i).length; 460 } 461 462 // add this split to the list that is returned 463 CombineFileSplit thissplit = new CombineFileSplit(fl, offset, 464 length, locations.toArray(new String[0])); 465 splitList.add(thissplit); 466 } 467 468 /** 469 * This is not implemented yet. 470 */ 471 public abstract RecordReader<K, V> createRecordReader(InputSplit split, 472 TaskAttemptContext context) throws IOException; 473 474 /** 475 * information about one file from the File System 476 */ 477 private static class OneFileInfo { 478 private long fileSize; // size of the file 479 private OneBlockInfo[] blocks; // all blocks in this file 480 481 OneFileInfo(Path path, Configuration conf, 482 boolean isSplitable, 483 HashMap<String, List<OneBlockInfo>> rackToBlocks, 484 HashMap<OneBlockInfo, String[]> blockToNodes, 485 HashMap<String, List<OneBlockInfo>> nodeToBlocks, 486 HashMap<String, Set<String>> rackToNodes, 487 long maxSize) 488 throws IOException { 489 this.fileSize = 0; 490 491 // get block locations from file system 492 FileSystem fs = path.getFileSystem(conf); 493 FileStatus stat = fs.getFileStatus(path); 494 BlockLocation[] locations = fs.getFileBlockLocations(stat, 0, 495 stat.getLen()); 496 // create a list of all block and their locations 497 if (locations == null) { 498 blocks = new OneBlockInfo[0]; 499 } else { 500 501 if(locations.length == 0) { 502 locations = new BlockLocation[] { new BlockLocation() }; 503 } 504 505 if (!isSplitable) { 506 // if the file is not splitable, just create the one block with 507 // full file length 508 blocks = new OneBlockInfo[1]; 509 fileSize = stat.getLen(); 510 blocks[0] = new OneBlockInfo(path, 0, fileSize, locations[0] 511 .getHosts(), locations[0].getTopologyPaths()); 512 } else { 513 ArrayList<OneBlockInfo> blocksList = new ArrayList<OneBlockInfo>( 514 locations.length); 515 for (int i = 0; i < locations.length; i++) { 516 fileSize += locations[i].getLength(); 517 518 // each split can be a maximum of maxSize 519 long left = locations[i].getLength(); 520 long myOffset = locations[i].getOffset(); 521 long myLength = 0; 522 do { 523 if (maxSize == 0) { 524 myLength = left; 525 } else { 526 if (left > maxSize && left < 2 * maxSize) { 527 // if remainder is between max and 2*max - then 528 // instead of creating splits of size max, left-max we 529 // create splits of size left/2 and left/2. This is 530 // a heuristic to avoid creating really really small 531 // splits. 532 myLength = left / 2; 533 } else { 534 myLength = Math.min(maxSize, left); 535 } 536 } 537 OneBlockInfo oneblock = new OneBlockInfo(path, myOffset, 538 myLength, locations[i].getHosts(), locations[i] 539 .getTopologyPaths()); 540 left -= myLength; 541 myOffset += myLength; 542 543 blocksList.add(oneblock); 544 } while (left > 0); 545 } 546 blocks = blocksList.toArray(new OneBlockInfo[blocksList.size()]); 547 } 548 549 for (OneBlockInfo oneblock : blocks) { 550 // add this block to the block --> node locations map 551 blockToNodes.put(oneblock, oneblock.hosts); 552 553 // For blocks that do not have host/rack information, 554 // assign to default rack. 555 String[] racks = null; 556 if (oneblock.hosts.length == 0) { 557 racks = new String[]{NetworkTopology.DEFAULT_RACK}; 558 } else { 559 racks = oneblock.racks; 560 } 561 562 // add this block to the rack --> block map 563 for (int j = 0; j < racks.length; j++) { 564 String rack = racks[j]; 565 List<OneBlockInfo> blklist = rackToBlocks.get(rack); 566 if (blklist == null) { 567 blklist = new ArrayList<OneBlockInfo>(); 568 rackToBlocks.put(rack, blklist); 569 } 570 blklist.add(oneblock); 571 if (!racks[j].equals(NetworkTopology.DEFAULT_RACK)) { 572 // Add this host to rackToNodes map 573 addHostToRack(rackToNodes, racks[j], oneblock.hosts[j]); 574 } 575 } 576 577 // add this block to the node --> block map 578 for (int j = 0; j < oneblock.hosts.length; j++) { 579 String node = oneblock.hosts[j]; 580 List<OneBlockInfo> blklist = nodeToBlocks.get(node); 581 if (blklist == null) { 582 blklist = new ArrayList<OneBlockInfo>(); 583 nodeToBlocks.put(node, blklist); 584 } 585 blklist.add(oneblock); 586 } 587 } 588 } 589 } 590 591 long getLength() { 592 return fileSize; 593 } 594 595 OneBlockInfo[] getBlocks() { 596 return blocks; 597 } 598 } 599 600 /** 601 * information about one block from the File System 602 */ 603 private static class OneBlockInfo { 604 Path onepath; // name of this file 605 long offset; // offset in file 606 long length; // length of this block 607 String[] hosts; // nodes on which this block resides 608 String[] racks; // network topology of hosts 609 610 OneBlockInfo(Path path, long offset, long len, 611 String[] hosts, String[] topologyPaths) { 612 this.onepath = path; 613 this.offset = offset; 614 this.hosts = hosts; 615 this.length = len; 616 assert (hosts.length == topologyPaths.length || 617 topologyPaths.length == 0); 618 619 // if the file system does not have any rack information, then 620 // use dummy rack location. 621 if (topologyPaths.length == 0) { 622 topologyPaths = new String[hosts.length]; 623 for (int i = 0; i < topologyPaths.length; i++) { 624 topologyPaths[i] = (new NodeBase(hosts[i], 625 NetworkTopology.DEFAULT_RACK)).toString(); 626 } 627 } 628 629 // The topology paths have the host name included as the last 630 // component. Strip it. 631 this.racks = new String[topologyPaths.length]; 632 for (int i = 0; i < topologyPaths.length; i++) { 633 this.racks[i] = (new NodeBase(topologyPaths[i])).getNetworkLocation(); 634 } 635 } 636 } 637 638 protected BlockLocation[] getFileBlockLocations( 639 FileSystem fs, FileStatus stat) throws IOException { 640 return fs.getFileBlockLocations(stat, 0, stat.getLen()); 641 } 642 643 private static void addHostToRack(HashMap<String, Set<String>> rackToNodes, 644 String rack, String host) { 645 Set<String> hosts = rackToNodes.get(rack); 646 if (hosts == null) { 647 hosts = new HashSet<String>(); 648 rackToNodes.put(rack, hosts); 649 } 650 hosts.add(host); 651 } 652 653 private Set<String> getHosts(Set<String> racks) { 654 Set<String> hosts = new HashSet<String>(); 655 for (String rack : racks) { 656 if (rackToNodes.containsKey(rack)) { 657 hosts.addAll(rackToNodes.get(rack)); 658 } 659 } 660 return hosts; 661 } 662 663 /** 664 * Accept a path only if any one of filters given in the 665 * constructor do. 666 */ 667 private static class MultiPathFilter implements PathFilter { 668 private List<PathFilter> filters; 669 670 public MultiPathFilter() { 671 this.filters = new ArrayList<PathFilter>(); 672 } 673 674 public MultiPathFilter(List<PathFilter> filters) { 675 this.filters = filters; 676 } 677 678 public void add(PathFilter one) { 679 filters.add(one); 680 } 681 682 public boolean accept(Path path) { 683 for (PathFilter filter : filters) { 684 if (filter.accept(path)) { 685 return true; 686 } 687 } 688 return false; 689 } 690 691 public String toString() { 692 StringBuffer buf = new StringBuffer(); 693 buf.append("["); 694 for (PathFilter f: filters) { 695 buf.append(f); 696 buf.append(","); 697 } 698 buf.append("]"); 699 return buf.toString(); 700 } 701 } 702 }