/*******************************************************************************
- * Copyright (c) 2011-2012 Ericsson, Ecole Polytechnique de Montreal and others
+ * Copyright (c) 2011-2013 Ericsson, Ecole Polytechnique de Montreal and others
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.Serializable;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
+import java.util.TreeSet;
import java.util.UUID;
+import org.eclipse.linuxtools.ctf.core.event.CTFCallsite;
import org.eclipse.linuxtools.ctf.core.event.CTFClock;
+import org.eclipse.linuxtools.ctf.core.event.EventDefinition;
+import org.eclipse.linuxtools.ctf.core.event.IEventDeclaration;
+import org.eclipse.linuxtools.ctf.core.event.io.BitBuffer;
import org.eclipse.linuxtools.ctf.core.event.types.ArrayDefinition;
import org.eclipse.linuxtools.ctf.core.event.types.Definition;
import org.eclipse.linuxtools.ctf.core.event.types.IDefinitionScope;
import org.eclipse.linuxtools.ctf.core.event.types.IntegerDefinition;
import org.eclipse.linuxtools.ctf.core.event.types.StructDeclaration;
import org.eclipse.linuxtools.ctf.core.event.types.StructDefinition;
-import org.eclipse.linuxtools.internal.ctf.core.Activator;
-import org.eclipse.linuxtools.internal.ctf.core.event.io.BitBuffer;
import org.eclipse.linuxtools.internal.ctf.core.event.metadata.exceptions.ParseException;
-import org.eclipse.linuxtools.internal.ctf.core.trace.Stream;
-import org.eclipse.linuxtools.internal.ctf.core.trace.StreamInput;
+import org.eclipse.linuxtools.internal.ctf.core.trace.StreamInputPacketIndex;
/**
- * <b><u>CTFTrace</u></b>
- * <p>
+ * A CTF trace on the file system.
+ *
* Represents a trace on the filesystem. It is responsible of parsing the
* metadata, creating declarations data structures, indexing the event packets
* (in other words, all the work that can be shared between readers), but the
* actual reading of events is left to TraceReader.
- *
+ *
* @author Matthew Khouzam
* @version $Revision: 1.0 $
*/
public class CTFTrace implements IDefinitionScope {
- // ------------------------------------------------------------------------
- // Attributes
- // ------------------------------------------------------------------------
-
- /*
- * (non-Javadoc)
- *
- * @see java.lang.Object#toString()
- */
@SuppressWarnings("nls")
@Override
public String toString() {
/**
* Packet header structure declaration
*/
- private StructDeclaration packetHeaderDecl;
+ private StructDeclaration packetHeaderDecl = null;
+
+ /**
+ * The clock of the trace
+ */
+ private CTFClock singleClock;
/**
* Packet header structure definition
- *
+ *
* This is only used when opening the trace files, to read the first packet
* header and see if they are valid trace files.
*/
/**
* Collection of streams contained in the trace.
*/
- private final HashMap<Long, Stream> streams;
+ private final Map<Long, Stream> streams = new HashMap<Long, Stream>();
/**
* Collection of environment variables set by the tracer
*/
- private final HashMap<String, String> environment;
+ private final Map<String, String> environment = new HashMap<String, String>();
/**
* Collection of all the clocks in a system.
*/
- private final HashMap<String, CTFClock> clocks;
+ private final Map<String, CTFClock> clocks = new HashMap<String, CTFClock>();
+
+ /** FileInputStreams to the streams */
+ private final List<FileInputStream> fileInputStreams = new LinkedList<FileInputStream>();
/** Handlers for the metadata files */
private final static FileFilter metadataFileFilter = new MetadataFileFilter();
- private final static Comparator<File> metadataComparator = new MetadataComparator();
+ private final static Comparator<File> metadataComparator = new MetadataComparator(); // $codepro.audit.disable
+ // fieldJavadoc
+
+ /** map of all the event types */
+ private final Map<Long,HashMap<Long, IEventDeclaration>> eventDecs = new HashMap<Long, HashMap<Long,IEventDeclaration>>();
+ /** map of all the event types */
+ private final Map<StreamInput,HashMap<Long, EventDefinition>> eventDefs = new HashMap<StreamInput, HashMap<Long,EventDefinition>>();
+ /** map of all the indexes */
+ private final Map<StreamInput, StreamInputPacketIndex> indexes = new HashMap<StreamInput, StreamInputPacketIndex>();
+
+ /** Callsite helpers */
+ private Map<String, LinkedList<CTFCallsite>> callsitesByName = new HashMap<String, LinkedList<CTFCallsite>>();
+ /** Callsite helpers */
+ private TreeSet<CTFCallsite> callsitesByIP = new TreeSet<CTFCallsite>();
+
+
// ------------------------------------------------------------------------
// Constructors
/**
* Trace constructor.
- *
+ *
* @param path
- * Filesystem path of the trace directory.
- * @throws IOException
+ * Filesystem path of the trace directory
+ * @throws CTFReaderException
+ * If no CTF trace was found at the path
*/
public CTFTrace(String path) throws CTFReaderException {
this(new File(path));
+
}
/**
* Trace constructor.
- *
+ *
* @param path
* Filesystem path of the trace directory.
* @throws CTFReaderException
+ * If no CTF trace was found at the path
*/
public CTFTrace(File path) throws CTFReaderException {
this.path = path;
this.metadata = new Metadata(this);
+ /* Set up the internal containers for this trace */
+ if (!this.path.exists()) {
+ throw new CTFReaderException("Trace (" + path.getPath() + ") doesn't exist. Deleted or moved?"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
if (!this.path.isDirectory()) {
throw new CTFReaderException("Path must be a valid directory"); //$NON-NLS-1$
}
-
- /* Set up the internal containers for this trace */
- streams = new HashMap<Long, Stream>();
- environment = new HashMap<String, String>();
- clocks = new HashMap<String, CTFClock>();
/* Open and parse the metadata file */
metadata.parse();
- if (Activator.getDefault() != null) {
- Activator.getDefault().log(metadata.toString());
- }
-
/* Open all the trace files */
/* Create the definitions needed to read things from the files */
if (packetHeaderDecl != null) {
/* List files not called metadata and not hidden. */
File[] files = path.listFiles(metadataFileFilter);
Arrays.sort(files, metadataComparator);
-
/* Try to open each file */
- for (File s : files) {
- openStreamInput(s);
+ for (File streamFile : files) {
+ openStreamInput(streamFile);
}
/* Create their index */
for (Map.Entry<Long, Stream> stream : streams.entrySet()) {
Set<StreamInput> inputs = stream.getValue().getStreamInputs();
for (StreamInput s : inputs) {
- s.createIndex();
+ /*
+ * Copy the events
+ */
+ Iterator<Entry<Long, IEventDeclaration>> it = s.getStream()
+ .getEvents().entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<Long, IEventDeclaration> pairs = it.next();
+ Long eventNum = pairs.getKey();
+ IEventDeclaration eventDec = pairs.getValue();
+ getEvents(s.getStream().getId()).put(eventNum, eventDec);
+ }
+
+ /*
+ * index the trace
+ */
+ s.setupIndex();
+ }
+ }
+ }
+
+ /**
+ * Dispose the trace
+ * @since 2.0
+ */
+ public void dispose() {
+ for (FileInputStream fis : fileInputStreams) {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ // do nothing it's ok, we tried to close it.
+ }
}
}
+ System.gc(); // Invoke GC to release MappedByteBuffer objects (Java bug JDK-4724038)
}
// ------------------------------------------------------------------------
// Getters/Setters/Predicates
// ------------------------------------------------------------------------
+ /**
+ * Gets an event declaration hash map for a given streamID
+ *
+ * @param streamId
+ * The ID of the stream from which to read
+ * @return The Hash map with the event declarations
+ */
+ public HashMap<Long, IEventDeclaration> getEvents(Long streamId) {
+ return eventDecs.get(streamId);
+ }
+
+ /**
+ * Gets an index for a given StreamInput
+ * @param id the StreamInput
+ * @return The index
+ */
+ StreamInputPacketIndex getIndex(StreamInput id){
+ if(! indexes.containsKey(id)){
+ indexes.put(id, new StreamInputPacketIndex());
+ }
+ return indexes.get(id);
+ }
+
+ /**
+ * Gets an event Declaration hashmap for a given StreamInput
+ * @param id the StreamInput
+ * @return the hashmap with the event definitions
+ * @since 2.0
+ */
+ public HashMap<Long, EventDefinition> getEventDefs(StreamInput id) {
+ if(! eventDefs.containsKey(id)){
+ eventDefs.put(id, new HashMap<Long, EventDefinition>());
+ }
+ return eventDefs.get(id);
+ }
+
+ /**
+ * Get an event by it's ID
+ *
+ * @param streamId
+ * The ID of the stream from which to read
+ * @param id
+ * the ID of the event
+ * @return the event declaration
+ * @since 2.0
+ */
+ public IEventDeclaration getEventType(long streamId, long id) {
+ return getEvents(streamId).get(id);
+ }
+
/**
* Method getStream gets the stream for a given id
- *
+ *
* @param id
* Long the id of the stream
* @return Stream the stream that we need
+ * @since 2.0
*/
public Stream getStream(Long id) {
return streams.get(id);
/**
* Method nbStreams gets the number of available streams
- *
+ *
* @return int the number of streams
*/
public int nbStreams() {
/**
* Method setMajor sets the major version of the trace (DO NOT USE)
- *
+ *
* @param major
* long the major version
*/
/**
* Method setMinor sets the minor version of the trace (DO NOT USE)
- *
+ *
* @param minor
* long the minor version
*/
/**
* Method setUUID sets the UUID of a trace
- *
+ *
* @param uuid
* UUID
*/
/**
* Method setByteOrder sets the byte order
- *
+ *
* @param byteOrder
* ByteOrder of the trace, can be little-endian or big-endian
*/
/**
* Method setPacketHeader sets the packet header of a trace (DO NOT USE)
- *
+ *
* @param packetHeader
* StructDeclaration the header in structdeclaration form
*/
/**
* Method majortIsSet is the major version number set?
- *
+ *
* @return boolean is the major set?
*/
public boolean majortIsSet() {
/**
* Method minorIsSet. is the minor version number set?
- *
+ *
* @return boolean is the minor set?
*/
public boolean minorIsSet() {
/**
* Method UUIDIsSet is the UUID set?
- *
+ *
* @return boolean is the UUID set?
*/
public boolean UUIDIsSet() {
/**
* Method byteOrderIsSet is the byteorder set?
- *
+ *
* @return boolean is the byteorder set?
*/
public boolean byteOrderIsSet() {
/**
* Method packetHeaderIsSet is the packet header set?
- *
+ *
* @return boolean is the packet header set?
*/
public boolean packetHeaderIsSet() {
/**
* Method getUUID gets the trace UUID
- *
+ *
* @return UUID gets the trace UUID
*/
public UUID getUUID() {
/**
* Method getMajor gets the trace major version
- *
+ *
* @return long gets the trace major version
*/
public long getMajor() {
/**
* Method getMinor gets the trace minor version
- *
+ *
* @return long gets the trace minor version
*/
public long getMinor() {
/**
* Method getByteOrder gets the trace byte order
- *
+ *
* @return ByteOrder gets the trace byte order
*/
public ByteOrder getByteOrder() {
/**
* Method getPacketHeader gets the trace packet header
- *
+ *
* @return StructDeclaration gets the trace packet header
*/
public StructDeclaration getPacketHeader() {
/**
* Method getTraceDirectory gets the trace directory
- *
+ *
* @return File the path in "File" format.
*/
public File getTraceDirectory() {
/**
* Method getStreams get all the streams in a map format.
- *
+ *
* @return Map<Long,Stream> a map of all the streams.
*/
public Map<Long, Stream> getStreams() {
/**
* Method getPath gets the path of the trace directory
- *
+ *
* @return String the path of the trace directory, in string format.
* @see java.io.File#getPath()
*/
/**
* Tries to open the given file, reads the first packet header of the file
* and check its validity.
- *
+ *
* @param streamFile
* A trace file in the trace directory.
+ * @param index
+ * Which index in the class' streamFileChannel array this file
+ * must use
* @throws CTFReaderException
*/
private void openStreamInput(File streamFile) throws CTFReaderException {
- FileChannel streamFileChannel;
MappedByteBuffer byteBuffer;
BitBuffer streamBitBuffer;
+ Stream stream;
+ FileChannel fc;
if (!streamFile.canRead()) {
throw new CTFReaderException("Unreadable file : " //$NON-NLS-1$
try {
/* Open the file and get the FileChannel */
- streamFileChannel = new FileInputStream(streamFile).getChannel();
+ FileInputStream fis = new FileInputStream(streamFile);
+ fileInputStreams.add(fis);
+ fc = fis.getChannel();
/* Map one memory page of 4 kiB */
- byteBuffer = streamFileChannel.map(MapMode.READ_ONLY, 0, 4096);
+ byteBuffer = fc.map(MapMode.READ_ONLY, 0, 4096);
} catch (IOException e) {
/* Shouldn't happen at this stage if every other check passed */
throw new CTFReaderException();
packetHeaderDef.read(streamBitBuffer);
/* Check the magic number */
- IntegerDefinition magicDef = (IntegerDefinition) packetHeaderDef.lookupDefinition("magic"); //$NON-NLS-1$
+ IntegerDefinition magicDef = (IntegerDefinition) packetHeaderDef
+ .lookupDefinition("magic"); //$NON-NLS-1$
int magic = (int) magicDef.getValue();
if (magic != Utils.CTF_MAGIC) {
throw new CTFReaderException("CTF magic mismatch"); //$NON-NLS-1$
}
/* Check UUID */
- ArrayDefinition uuidDef = (ArrayDefinition) packetHeaderDef.lookupDefinition("uuid"); //$NON-NLS-1$
- assert ((uuidDef != null) && (uuidDef.getDeclaration().getLength() == Utils.UUID_LEN));
+ ArrayDefinition uuidDef = (ArrayDefinition) packetHeaderDef
+ .lookupDefinition("uuid"); //$NON-NLS-1$
if (uuidDef != null) {
byte[] uuidArray = new byte[Utils.UUID_LEN];
for (int i = 0; i < Utils.UUID_LEN; i++) {
- IntegerDefinition uuidByteDef = (IntegerDefinition) uuidDef.getElem(i);
+ IntegerDefinition uuidByteDef = (IntegerDefinition) uuidDef
+ .getElem(i);
uuidArray[i] = (byte) uuidByteDef.getValue();
}
}
}
- /* Read stream ID */
- // TODO: it hasn't been checked that the stream_id field exists and
- // is an unsigned
- // integer
- IntegerDefinition streamIDDef = (IntegerDefinition) packetHeaderDef.lookupDefinition("stream_id"); //$NON-NLS-1$
- assert (streamIDDef != null);
-
- long streamID = streamIDDef.getValue();
-
- /* Get the stream to which this trace file belongs to */
- Stream stream = streams.get(streamID);
+ /* Read the stream ID */
+ Definition streamIDDef = packetHeaderDef.lookupDefinition("stream_id"); //$NON-NLS-1$
- /* Create the stream input */
- StreamInput streamInput = new StreamInput(stream,
- streamFileChannel, streamFile);
+ if (streamIDDef instanceof IntegerDefinition) { //this doubles as a null check
+ long streamID = ((IntegerDefinition) streamIDDef).getValue();
+ stream = streams.get(streamID);
+ } else {
+ /* No stream_id in the packet header */
+ stream = streams.get(null);
+ }
- /* Add a reference to the streamInput in the stream */
- stream.addInput(streamInput);
} else {
/* No packet header, we suppose there is only one stream */
- Stream stream = streams.get(null);
+ stream = streams.get(null);
+ }
- /* Create the stream input */
- StreamInput streamInput = new StreamInput(stream,
- streamFileChannel, streamFile);
+ /* Create the stream input */
+ StreamInput streamInput = new StreamInput(stream, fc, streamFile);
- /* Add a reference to the streamInput in the stream */
- stream.addInput(streamInput);
- }
+ /* Add a reference to the streamInput in the stream */
+ stream.addInput(streamInput);
}
/**
* Looks up a definition from packet
- *
+ *
* @param lookupPath
* String
* @return Definition
/**
* Adds a new stream to the trace.
- *
+ *
* @param stream
* A stream object.
- *
* @throws ParseException
+ * If there was some problem reading the metadata
+ * @since 2.0
*/
public void addStream(Stream stream) throws ParseException {
/* It should be ok now. */
streams.put(stream.getId(), stream);
+ eventDecs.put(stream.getId(), new HashMap<Long,IEventDeclaration>());
}
- public HashMap<String, String> getEnvironment() {
+ /**
+ * gets the Environment variables from the trace metadata (See CTF spec)
+ * @return the environment variables in a map form (key value)
+ * @since 2.0
+ */
+ public Map<String, String> getEnvironment() {
return environment;
}
+ /**
+ * Look up a specific environment variable
+ * @param key the key to look for
+ * @return the value of the variable, can be null.
+ */
public String lookupEnvironment(String key) {
return environment.get(key);
}
+ /**
+ * Add a variable to the environment variables
+ * @param varName the name of the variable
+ * @param varValue the value of the variable
+ */
public void addEnvironmentVar(String varName, String varValue) {
environment.put(varName, varValue);
}
+ /**
+ * Add a clock to the clock list
+ * @param nameValue the name of the clock (full name with scope)
+ * @param ctfClock the clock
+ */
public void addClock(String nameValue, CTFClock ctfClock) {
clocks.put(nameValue, ctfClock);
}
+ /**
+ * gets the clock with a specific name
+ * @param name the name of the clock.
+ * @return the clock
+ */
public CTFClock getClock(String name) {
return clocks.get(name);
}
- public CTFClock getClock() {
+
+
+
+ /**
+ * gets the clock if there is only one. (this is 100% of the use cases as of
+ * June 2012)
+ *
+ * @return the clock
+ */
+ public final CTFClock getClock() {
if (clocks.size() == 1) {
- String key = (String) clocks.keySet().toArray()[0];
- return clocks.get(key);
+ singleClock = clocks.get(clocks.keySet().iterator().next());
+ return singleClock;
}
return null;
}
- public long getOffset() {
+ /**
+ * gets the time offset of a clock with respect to UTC in nanoseconds
+ *
+ * @return the time offset of a clock with respect to UTC in nanoseconds
+ */
+ public final long getOffset() {
if (getClock() == null) {
return 0;
}
- return (Long) getClock().getProperty("offset"); //$NON-NLS-1$
+ return singleClock.getClockOffset();
+ }
+
+ /**
+ * gets the time offset of a clock with respect to UTC in nanoseconds
+ *
+ * @return the time offset of a clock with respect to UTC in nanoseconds
+ */
+ private final double getTimeScale() {
+ if (getClock() == null) {
+ return 1.0;
+ }
+ return singleClock.getClockScale();
+ }
+
+ /**
+ * Does the trace need to time scale?
+ *
+ * @return if the trace is in ns or cycles.
+ */
+ private final boolean clockNeedsScale() {
+ if (getClock() == null) {
+ return false;
+ }
+ return singleClock.isClockScaled();
+ }
+
+ /**
+ * the inverse clock for returning to a scale.
+ *
+ * @return 1.0 / scale
+ */
+ private final double getInverseTimeScale() {
+ if (getClock() == null) {
+ return 1.0;
+ }
+ return singleClock.getClockAntiScale();
+ }
+
+ /**
+ * @param cycles
+ * clock cycles since boot
+ * @return time in nanoseconds UTC offset
+ * @since 2.0
+ */
+ public long timestampCyclesToNanos(long cycles) {
+ long retVal = cycles + getOffset();
+ /*
+ * this fix is since quite often the offset will be > than 53 bits and
+ * therefore the conversion will be lossy
+ */
+ if (clockNeedsScale()) {
+ retVal = (long) (retVal * getTimeScale());
+ }
+ return retVal;
+ }
+
+ /**
+ * @param nanos
+ * time in nanoseconds UTC offset
+ * @return clock cycles since boot.
+ * @since 2.0
+ */
+ public long timestampNanoToCycles(long nanos) {
+ long retVal;
+ /*
+ * this fix is since quite often the offset will be > than 53 bits and
+ * therefore the conversion will be lossy
+ */
+ if (clockNeedsScale()) {
+ retVal = (long) (nanos * getInverseTimeScale());
+ } else {
+ retVal = nanos;
+ }
+ return retVal - getOffset();
+ }
+
+ /**
+ * Does a given stream contain any events?
+ * @param id the stream ID
+ * @return true if the stream has events.
+ */
+ public boolean hasEvents(Long id){
+ return eventDecs.containsKey(id);
+ }
+
+ /**
+ * Add an event declaration map to the events map.
+ * @param id the id of a stream
+ * @return the hashmap containing events.
+ */
+ public HashMap<Long, IEventDeclaration> createEvents(Long id){
+ HashMap<Long, IEventDeclaration> value = eventDecs.get(id);
+ if( value == null ) {
+ value = new HashMap<Long, IEventDeclaration>();
+ eventDecs.put(id, value);
+ }
+ return value;
+ }
+
+ /**
+ * Adds a callsite
+ *
+ * @param eventName
+ * the event name of the callsite
+ * @param funcName
+ * the name of the callsite function
+ * @param ip
+ * the ip of the callsite
+ * @param fileName
+ * the filename of the callsite
+ * @param lineNumber
+ * the line number of the callsite
+ */
+ public void addCallsite(String eventName, String funcName, long ip,
+ String fileName, long lineNumber) {
+ final CTFCallsite cs = new CTFCallsite(eventName, funcName, ip,
+ fileName, lineNumber);
+ LinkedList<CTFCallsite> csl = callsitesByName.get(eventName);
+ if (csl == null) {
+ csl = new LinkedList<CTFCallsite>();
+ callsitesByName.put(eventName, csl);
+ }
+
+ ListIterator<CTFCallsite> iter = csl.listIterator();
+ int index = 0;
+ for (; index < csl.size(); index++) {
+ if (iter.next().compareTo(cs) < 0) {
+ break;
+ }
+ }
+
+ csl.add(index, cs);
+
+ callsitesByIP.add(cs);
+ }
+
+ /**
+ * Gets the list of callsites associated to an event name. O(1)
+ *
+ * @param eventName
+ * the event name
+ * @return the callsite list can be empty
+ * @since 1.2
+ */
+ public List<CTFCallsite> getCallsiteCandidates(String eventName) {
+ LinkedList<CTFCallsite> retVal = callsitesByName.get(eventName);
+ if( retVal == null ) {
+ retVal = new LinkedList<CTFCallsite>();
+ }
+ return retVal;
+ }
+
+ /**
+ * The I'm feeling lucky of getCallsiteCandidates O(1)
+ *
+ * @param eventName
+ * the event name
+ * @return the first callsite that has that event name, can be null
+ * @since 1.2
+ */
+ public CTFCallsite getCallsite(String eventName) {
+ LinkedList<CTFCallsite> callsites = callsitesByName.get(eventName);
+ if (callsites != null) {
+ return callsites.getFirst();
+ }
+ return null;
+ }
+
+ /**
+ * Gets a callsite from the instruction pointer O(log(n))
+ *
+ * @param ip
+ * the instruction pointer to lookup
+ * @return the callsite just before that IP in the list remember the IP is
+ * backwards on X86, can be null if no callsite is before the IP.
+ * @since 1.2
+ */
+ public CTFCallsite getCallsite(long ip) {
+ CTFCallsite cs = new CTFCallsite(null, null, ip, null, 0L);
+ return callsitesByIP.ceiling(cs);
+ }
+
+ /**
+ * Gets a callsite using the event name and instruction pointer O(log(n))
+ *
+ * @param eventName
+ * the name of the event
+ * @param ip
+ * the instruction pointer
+ * @return the closest matching callsite, can be null
+ */
+ public CTFCallsite getCallsite(String eventName, long ip) {
+ final LinkedList<CTFCallsite> candidates = callsitesByName.get(eventName);
+ final CTFCallsite dummyCs = new CTFCallsite(null, null, ip, null, -1);
+ final int pos = Collections.binarySearch(candidates, dummyCs)+1;
+ if( pos >= candidates.size()) {
+ return null;
+ }
+ return candidates.get(pos);
}
}
}
-class MetadataComparator implements Comparator<File> {
+class MetadataComparator implements Comparator<File>, Serializable {
+
+ private static final long serialVersionUID = 1L;
@Override
public int compare(File o1, File o2) {