Here's how I solved the problem using a custom InputFormat... the key
part is in listStatus(), where we traverse the directory tree.  Since
HDFS doesn't have links this code is probably safe, but if you have a
filesystem with cycles you will get trapped.

Ian

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ArrayDeque;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.PathFilter;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.RecordReader;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.mapred.InputSplit;
import org.apache.hadoop.mapred.InvalidInputException;
import org.apache.hadoop.mapred.LineRecordReader;

public class TrecWebInputFormat extends FileInputFormat<DocLocation, Text> {
    @Override 
        public boolean isSplitable(FileSystem fs, Path filename) {
        return false;
    }
                                       
    @Override 
        public RecordReader<DocLocation, Text> 
        getRecordReader(InputSplit split, JobConf job, Reporter reporter)
        throws IOException {
        return new TrecWebRecordReader(job, (FileSplit)split);
    }

    // The following are incomprehensibly private in FileInputFormat...
    private static final PathFilter hiddenFileFilter = new PathFilter(){
            public boolean accept(Path p){
                String name = p.getName(); 
                return !name.startsWith("_") && !name.startsWith("."); 
            }
        }; 
    
    /**
     * Proxy PathFilter that accepts a path only if all filters given in the
     * constructor do. Used by the listPaths() to apply the built-in
     * hiddenFileFilter together with a user provided one (if any).
     */
    private static class MultiPathFilter implements PathFilter {
        private List<PathFilter> filters;
        
        public MultiPathFilter(List<PathFilter> filters) {
            this.filters = filters;
        }
        
        public boolean accept(Path path) {
            for (PathFilter filter : filters) {
                if (!filter.accept(path)) {
                    return false;
                }
            }
            return true;
        }
    }

    
    @Override
        protected FileStatus[] listStatus(JobConf job) 
        throws IOException {
        Path[] dirs = getInputPaths(job);
        if (dirs.length == 0) {
            throw new IOException("No input paths specified in job");
        }

        List<FileStatus> result = new ArrayList<FileStatus>();
        List<IOException> errors = new ArrayList<IOException>();
        ArrayDeque<FileStatus> stats = new ArrayDeque<FileStatus>(dirs.length);

        // creates a MultiPathFilter with the hiddenFileFilter and the
        // user provided one (if any).
        List<PathFilter> filters = new ArrayList<PathFilter>();
        filters.add(hiddenFileFilter);
        PathFilter jobFilter = getInputPathFilter(job);
        if (jobFilter != null) {
            filters.add(jobFilter);
        }
        PathFilter inputFilter = new MultiPathFilter(filters);

        // Set up traversal from input paths, which may be globs
        for (Path p: dirs) {
            FileSystem fs = p.getFileSystem(job);
            FileStatus[] matches = fs.globStatus(p, inputFilter);
            if (matches == null) {
                errors.add(new IOException("Input path does not exist: " + p));
            } else if (matches.length == 0) {
                errors.add(new IOException("Input Pattern " + p + " matches 0 
files"));
            } else {
                for (FileStatus globStat: matches) {
                    stats.add(globStat);
                }
            }
        }

        while (!stats.isEmpty()) {
            FileStatus stat = stats.pop();
            if (stat.isDir()) {
                FileSystem fs = stat.getPath().getFileSystem(job);
                for (FileStatus sub: fs.listStatus(stat.getPath(), 
                                                   inputFilter)) {
                    stats.push(sub);
                } 
            } else {
                result.add(stat);
            }
        }

        if (!errors.isEmpty()) {
            throw new InvalidInputException(errors);
        }
        LOG.info("Total input paths to process : " + result.size()); 
        return result.toArray(new FileStatus[result.size()]);
    }
        

    public static class TrecWebRecordReader 
        implements RecordReader<DocLocation, Text> {
        private CompressionCodecFactory compressionCodecs = null;
        private long start;
        private long end;
        private long pos;
        private Path file;
        private LineRecordReader.LineReader in;
        
        public TrecWebRecordReader(JobConf job, FileSplit split)
            throws IOException {
            file = split.getPath();
            start = 0;
            end = split.getLength();
            compressionCodecs = new CompressionCodecFactory(job);
            CompressionCodec codec = compressionCodecs.getCodec(file);

            FileSystem fs = file.getFileSystem(job);
            FSDataInputStream fileIn = fs.open(file);

            if (codec != null) {
                in = new 
LineRecordReader.LineReader(codec.createInputStream(fileIn), job);
            } else {
                in = new LineRecordReader.LineReader(fileIn, job);
            }
            pos = 0;
        }
    
        public DocLocation createKey() { 
            return new DocLocation(); 
        }
        public Text createValue() { 
            return new Text(); 
        }

        public synchronized boolean next(DocLocation key, Text value) 
            throws IOException {
            Text line = new Text();
            StringBuilder buf = new StringBuilder();
            boolean in_doc = false;

            try {
                while (true) {
                    int size = in.readLine(line);
                    if (size <= 0)
                        break;
                    pos += size;
                    if (!in_doc && line.find("<DOC>") >= 0) {
                        in_doc = true;
                        key.offset = pos;
                        key.filename = file.toString();
                    }
                    if (in_doc) {
                        buf.append(line.toString()).append("\n");
                        if (line.find("</DOC>") >= 0) {
                            in_doc = false;
                            break;
                        }
                    }
                }
            } catch (java.io.EOFException e) {
            }

            if (buf.length() > 0) {
                value.set(buf.toString());
                key.length = value.getLength();
                return true;
            } else {
                return false;
            }
        }

        public synchronized long getPos() throws IOException {
            return pos;
        }
        
        public float getProgress() {
            return Math.min(1.0f, (pos) / (float)(end));
        }

        public synchronized void close() throws IOException {
            if (in != null) {
                in.close();
            }
        }
    }
}

Reply via email to