9d82709e13cd247e4180beeda48d6dafa2dc578a
[deliverable/tracecompass.git] / ctf / org.eclipse.tracecompass.ctf.core / src / org / eclipse / tracecompass / ctf / core / trace / CTFTrace.java
1 /*******************************************************************************
2 * Copyright (c) 2011, 2014 Ericsson, Ecole Polytechnique de Montreal and others
3 *
4 * All rights reserved. This program and the accompanying materials are made
5 * available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *
9 * Contributors:
10 * Matthew Khouzam - Initial API and implementation
11 * Alexandre Montplaisir - Initial API and implementation
12 * Simon Delisle - Replace LinkedList by TreeSet in callsitesByName attribute
13 *******************************************************************************/
14
15 package org.eclipse.tracecompass.ctf.core.trace;
16
17 import java.io.File;
18 import java.io.FileFilter;
19 import java.io.IOException;
20 import java.io.Serializable;
21 import java.nio.ByteBuffer;
22 import java.nio.ByteOrder;
23 import java.nio.channels.FileChannel;
24 import java.nio.channels.FileChannel.MapMode;
25 import java.nio.file.StandardOpenOption;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.HashMap;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.UUID;
34
35 import org.eclipse.tracecompass.ctf.core.CTFException;
36 import org.eclipse.tracecompass.ctf.core.CTFStrings;
37 import org.eclipse.tracecompass.ctf.core.event.CTFClock;
38 import org.eclipse.tracecompass.ctf.core.event.IEventDeclaration;
39 import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer;
40 import org.eclipse.tracecompass.ctf.core.event.metadata.DeclarationScope;
41 import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope;
42 import org.eclipse.tracecompass.ctf.core.event.scope.ILexicalScope;
43 import org.eclipse.tracecompass.ctf.core.event.types.AbstractArrayDefinition;
44 import org.eclipse.tracecompass.ctf.core.event.types.Definition;
45 import org.eclipse.tracecompass.ctf.core.event.types.IDefinition;
46 import org.eclipse.tracecompass.ctf.core.event.types.IntegerDefinition;
47 import org.eclipse.tracecompass.ctf.core.event.types.StructDeclaration;
48 import org.eclipse.tracecompass.ctf.core.event.types.StructDefinition;
49 import org.eclipse.tracecompass.internal.ctf.core.SafeMappedByteBuffer;
50 import org.eclipse.tracecompass.internal.ctf.core.event.metadata.MetadataStrings;
51 import org.eclipse.tracecompass.internal.ctf.core.event.metadata.exceptions.ParseException;
52 import org.eclipse.tracecompass.internal.ctf.core.trace.Utils;
53
54 /**
55 * A CTF trace on the file system.
56 *
57 * Represents a trace on the filesystem. It is responsible of parsing the
58 * metadata, creating declarations data structures, indexing the event packets
59 * (in other words, all the work that can be shared between readers), but the
60 * actual reading of events is left to TraceReader.
61 *
62 * TODO: internalize CTFTrace and DeclarationScope
63 *
64 * @author Matthew Khouzam
65 * @version $Revision: 1.0 $
66 */
67 public class CTFTrace implements IDefinitionScope {
68
69 @Override
70 public String toString() {
71 /* Only for debugging, shouldn't be externalized */
72 return "CTFTrace [path=" + fPath + ", major=" + fMajor + ", minor=" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
73 + fMinor + ", uuid=" + fUuid + "]"; //$NON-NLS-1$ //$NON-NLS-2$
74 }
75
76 /**
77 * The trace directory on the filesystem.
78 */
79 private final File fPath;
80
81 /**
82 * Major CTF version number
83 */
84 private Long fMajor;
85
86 /**
87 * Minor CTF version number
88 */
89 private Long fMinor;
90
91 /**
92 * Trace UUID
93 */
94 private UUID fUuid;
95
96 /**
97 * Trace byte order
98 */
99 private ByteOrder fByteOrder;
100
101 /**
102 * Packet header structure declaration
103 */
104 private StructDeclaration fPacketHeaderDecl = null;
105
106 /**
107 * The clock of the trace
108 */
109 private CTFClock fSingleClock = null;
110
111 /**
112 * Packet header structure definition
113 *
114 * This is only used when opening the trace files, to read the first packet
115 * header and see if they are valid trace files.
116 */
117 private StructDefinition fPacketHeaderDef;
118
119 /**
120 * Collection of streams contained in the trace.
121 */
122 private final Map<Long, CTFStream> fStreams = new HashMap<>();
123
124 /**
125 * Collection of environment variables set by the tracer
126 */
127 private final Map<String, String> fEnvironment = new HashMap<>();
128
129 /**
130 * Collection of all the clocks in a system.
131 */
132 private final Map<String, CTFClock> fClocks = new HashMap<>();
133
134 /** Handlers for the metadata files */
135 private static final FileFilter METADATA_FILE_FILTER = new MetadataFileFilter();
136 private static final Comparator<File> METADATA_COMPARATOR = new MetadataComparator();
137
138 private final DeclarationScope fScope = new DeclarationScope(null, MetadataStrings.TRACE);
139
140 // ------------------------------------------------------------------------
141 // Constructors
142 // ------------------------------------------------------------------------
143
144 /**
145 * Trace constructor.
146 *
147 * @param path
148 * Filesystem path of the trace directory
149 * @throws CTFException
150 * If no CTF trace was found at the path
151 */
152 public CTFTrace(String path) throws CTFException {
153 this(new File(path));
154 }
155
156 /**
157 * Trace constructor.
158 *
159 * @param path
160 * Filesystem path of the trace directory.
161 * @throws CTFException
162 * If no CTF trace was found at the path
163 */
164 public CTFTrace(File path) throws CTFException {
165 fPath = path;
166 final Metadata metadata = new Metadata(this);
167
168 /* Set up the internal containers for this trace */
169 if (!fPath.exists()) {
170 throw new CTFException("Trace (" + path.getPath() + ") doesn't exist. Deleted or moved?"); //$NON-NLS-1$ //$NON-NLS-2$
171 }
172
173 if (!fPath.isDirectory()) {
174 throw new CTFException("Path must be a valid directory"); //$NON-NLS-1$
175 }
176
177 /* Open and parse the metadata file */
178 metadata.parseFile();
179
180 init(path);
181 }
182
183 /**
184 * Streamed constructor
185 */
186 public CTFTrace() {
187 fPath = null;
188 }
189
190 private void init(File path) throws CTFException {
191
192 /* Open all the trace files */
193
194 /* List files not called metadata and not hidden. */
195 File[] files = path.listFiles(METADATA_FILE_FILTER);
196 Arrays.sort(files, METADATA_COMPARATOR);
197
198 /* Try to open each file */
199 for (File streamFile : files) {
200 openStreamInput(streamFile);
201 }
202
203 /* Create their index */
204 for (CTFStream stream : getStreams()) {
205 Set<CTFStreamInput> inputs = stream.getStreamInputs();
206 for (CTFStreamInput s : inputs) {
207 addStream(s);
208 }
209 }
210 }
211
212 // ------------------------------------------------------------------------
213 // Getters/Setters/Predicates
214 // ------------------------------------------------------------------------
215
216 /**
217 * Gets an event declaration list for a given streamID
218 *
219 * @param streamId
220 * The ID of the stream from which to read
221 * @return The list of event declarations
222 */
223 public Collection<IEventDeclaration> getEventDeclarations(Long streamId) {
224 return fStreams.get(streamId).getEventDeclarations();
225 }
226
227 /**
228 * Method getStream gets the stream for a given id
229 *
230 * @param id
231 * Long the id of the stream
232 * @return Stream the stream that we need
233 */
234 public CTFStream getStream(Long id) {
235 if (id == null) {
236 return fStreams.get(0L);
237 }
238 return fStreams.get(id);
239 }
240
241 /**
242 * Method nbStreams gets the number of available streams
243 *
244 * @return int the number of streams
245 */
246 public int nbStreams() {
247 return fStreams.size();
248 }
249
250 /**
251 * Method setMajor sets the major version of the trace (DO NOT USE)
252 *
253 * @param major
254 * long the major version
255 */
256 public void setMajor(long major) {
257 fMajor = major;
258 }
259
260 /**
261 * Method setMinor sets the minor version of the trace (DO NOT USE)
262 *
263 * @param minor
264 * long the minor version
265 */
266 public void setMinor(long minor) {
267 fMinor = minor;
268 }
269
270 /**
271 * Method setUUID sets the UUID of a trace
272 *
273 * @param uuid
274 * UUID
275 */
276 public void setUUID(UUID uuid) {
277 fUuid = uuid;
278 }
279
280 /**
281 * Method setByteOrder sets the byte order
282 *
283 * @param byteOrder
284 * ByteOrder of the trace, can be little-endian or big-endian
285 */
286 public void setByteOrder(ByteOrder byteOrder) {
287 fByteOrder = byteOrder;
288 }
289
290 /**
291 * Method setPacketHeader sets the packet header of a trace (DO NOT USE)
292 *
293 * @param packetHeader
294 * StructDeclaration the header in structdeclaration form
295 */
296 public void setPacketHeader(StructDeclaration packetHeader) {
297 fPacketHeaderDecl = packetHeader;
298 }
299
300 /**
301 * Method majorIsSet is the major version number set?
302 *
303 * @return boolean is the major set?
304 */
305 public boolean majorIsSet() {
306 return fMajor != null;
307 }
308
309 /**
310 * Method minorIsSet. is the minor version number set?
311 *
312 * @return boolean is the minor set?
313 */
314 public boolean minorIsSet() {
315 return fMinor != null;
316 }
317
318 /**
319 * Method UUIDIsSet is the UUID set?
320 *
321 * @return boolean is the UUID set?
322 */
323 public boolean uuidIsSet() {
324 return fUuid != null;
325 }
326
327 /**
328 * Method byteOrderIsSet is the byteorder set?
329 *
330 * @return boolean is the byteorder set?
331 */
332 public boolean byteOrderIsSet() {
333 return fByteOrder != null;
334 }
335
336 /**
337 * Method packetHeaderIsSet is the packet header set?
338 *
339 * @return boolean is the packet header set?
340 */
341 public boolean packetHeaderIsSet() {
342 return fPacketHeaderDecl != null;
343 }
344
345 /**
346 * Method getUUID gets the trace UUID
347 *
348 * @return UUID gets the trace UUID
349 */
350 public UUID getUUID() {
351 return fUuid;
352 }
353
354 /**
355 * Method getMajor gets the trace major version
356 *
357 * @return long gets the trace major version
358 */
359 public long getMajor() {
360 return fMajor;
361 }
362
363 /**
364 * Method getMinor gets the trace minor version
365 *
366 * @return long gets the trace minor version
367 */
368 public long getMinor() {
369 return fMinor;
370 }
371
372 /**
373 * Method getByteOrder gets the trace byte order
374 *
375 * @return ByteOrder gets the trace byte order
376 */
377 public final ByteOrder getByteOrder() {
378 return fByteOrder;
379 }
380
381 /**
382 * Method getPacketHeader gets the trace packet header
383 *
384 * @return StructDeclaration gets the trace packet header
385 */
386 public StructDeclaration getPacketHeader() {
387 return fPacketHeaderDecl;
388 }
389
390 /**
391 * Method getTraceDirectory gets the trace directory
392 *
393 * @return File the path in "File" format.
394 */
395 public File getTraceDirectory() {
396 return fPath;
397 }
398
399 /**
400 * Get all the streams as an iterable.
401 *
402 * @return Iterable&lt;Stream&gt; an iterable over streams.
403 */
404 public Iterable<CTFStream> getStreams() {
405 return fStreams.values();
406 }
407
408 /**
409 * Method getPath gets the path of the trace directory
410 *
411 * @return String the path of the trace directory, in string format.
412 * @see java.io.File#getPath()
413 */
414 public String getPath() {
415 return (fPath != null) ? fPath.getPath() : ""; //$NON-NLS-1$
416 }
417
418 // ------------------------------------------------------------------------
419 // Operations
420 // ------------------------------------------------------------------------
421
422 private void addStream(CTFStreamInput s) {
423
424 /*
425 * add the stream
426 */
427 CTFStream stream = s.getStream();
428 fStreams.put(stream.getId(), stream);
429
430 /*
431 * index the trace
432 */
433 s.setupIndex();
434 }
435
436 /**
437 * Tries to open the given file, reads the first packet header of the file
438 * and check its validity. This will add a file to a stream as a streaminput
439 *
440 * @param streamFile
441 * A trace file in the trace directory.
442 * @param index
443 * Which index in the class' streamFileChannel array this file
444 * must use
445 * @throws CTFException
446 * if there is a file error
447 */
448 private CTFStream openStreamInput(File streamFile) throws CTFException {
449 ByteBuffer byteBuffer;
450 BitBuffer streamBitBuffer;
451 CTFStream stream;
452
453 if (!streamFile.canRead()) {
454 throw new CTFException("Unreadable file : " //$NON-NLS-1$
455 + streamFile.getPath());
456 }
457 if (streamFile.length() == 0) {
458 return null;
459 }
460 try (FileChannel fc = FileChannel.open(streamFile.toPath(), StandardOpenOption.READ)) {
461 /* Map one memory page of 4 kiB */
462 byteBuffer = SafeMappedByteBuffer.map(fc, MapMode.READ_ONLY, 0, (int) Math.min(fc.size(), 4096L));
463 if (byteBuffer == null) {
464 throw new IllegalStateException("Failed to allocate memory"); //$NON-NLS-1$
465 }
466 /* Create a BitBuffer with this mapping and the trace byte order */
467 streamBitBuffer = new BitBuffer(byteBuffer, this.getByteOrder());
468 if (fPacketHeaderDecl != null) {
469 /* Read the packet header */
470 fPacketHeaderDef = fPacketHeaderDecl.createDefinition(this, ILexicalScope.PACKET_HEADER, streamBitBuffer);
471 }
472 } catch (IOException e) {
473 /* Shouldn't happen at this stage if every other check passed */
474 throw new CTFException(e);
475 }
476 if (fPacketHeaderDef != null) {
477 validateMagicNumber(fPacketHeaderDef);
478
479 validateUUID(fPacketHeaderDef);
480
481 /* Read the stream ID */
482 IDefinition streamIDDef = fPacketHeaderDef.lookupDefinition("stream_id"); //$NON-NLS-1$
483
484 if (streamIDDef instanceof IntegerDefinition) {
485 /* This doubles as a null check */
486 long streamID = ((IntegerDefinition) streamIDDef).getValue();
487 stream = fStreams.get(streamID);
488 } else {
489 /* No stream_id in the packet header */
490 stream = getStream(null);
491 }
492
493 } else {
494 /* No packet header, we suppose there is only one stream */
495 stream = getStream(null);
496 }
497
498 if (stream == null) {
499 throw new CTFException("Unexpected end of stream"); //$NON-NLS-1$
500 }
501
502 /*
503 * Create the stream input and add a reference to the streamInput in the
504 * stream.
505 */
506 stream.addInput(new CTFStreamInput(stream, streamFile));
507
508 return stream;
509 }
510
511 private void validateUUID(StructDefinition packetHeaderDef) throws CTFException {
512 IDefinition lookupDefinition = packetHeaderDef.lookupDefinition("uuid"); //$NON-NLS-1$
513 AbstractArrayDefinition uuidDef = (AbstractArrayDefinition) lookupDefinition;
514 if (uuidDef != null) {
515 UUID otheruuid = Utils.getUUIDfromDefinition(uuidDef);
516 if (!fUuid.equals(otheruuid)) {
517 throw new CTFException("UUID mismatch"); //$NON-NLS-1$
518 }
519 }
520 }
521
522 private static void validateMagicNumber(StructDefinition packetHeaderDef) throws CTFException {
523 IntegerDefinition magicDef = (IntegerDefinition) packetHeaderDef.lookupDefinition(CTFStrings.MAGIC);
524 if (magicDef != null) {
525 int magic = (int) magicDef.getValue();
526 if (magic != Utils.CTF_MAGIC) {
527 throw new CTFException("CTF magic mismatch"); //$NON-NLS-1$
528 }
529 }
530 }
531
532 // ------------------------------------------------------------------------
533 // IDefinitionScope
534 // ------------------------------------------------------------------------
535
536 /**
537 * @since 1.0
538 */
539 @Override
540 public ILexicalScope getScopePath() {
541 return ILexicalScope.TRACE;
542 }
543
544 /**
545 * Looks up a definition from packet
546 *
547 * @param lookupPath
548 * String
549 * @return Definition
550 * @see org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope#lookupDefinition(String)
551 */
552 @Override
553 public Definition lookupDefinition(String lookupPath) {
554 if (lookupPath.equals(ILexicalScope.TRACE_PACKET_HEADER.getPath())) {
555 return fPacketHeaderDef;
556 }
557 return null;
558 }
559
560 // ------------------------------------------------------------------------
561 // Live trace reading
562 // ------------------------------------------------------------------------
563
564 /**
565 * Add a new stream file to support new streams while the trace is being
566 * read.
567 *
568 * @param streamFile
569 * the file of the stream
570 * @throws CTFException
571 * A stream had an issue being read
572 */
573 public void addStreamFile(File streamFile) throws CTFException {
574 openStreamInput(streamFile);
575 }
576
577 /**
578 * Registers a new stream to the trace.
579 *
580 * @param stream
581 * A stream object.
582 * @throws ParseException
583 * If there was some problem reading the metadata
584 */
585 public void addStream(CTFStream stream) throws ParseException {
586 /*
587 * If there is already a stream without id (the null key), it must be
588 * the only one
589 */
590 if (fStreams.get(null) != null) {
591 throw new ParseException("Stream without id with multiple streams"); //$NON-NLS-1$
592 }
593
594 /*
595 * If the stream we try to add has no key set, it must be the only one.
596 * Thus, if the streams container is not empty, it is not valid.
597 */
598 if ((!stream.isIdSet()) && (!fStreams.isEmpty())) {
599 throw new ParseException("Stream without id with multiple streams"); //$NON-NLS-1$
600 }
601
602 /*
603 * If a stream with the same ID already exists, it is not valid.
604 */
605 CTFStream existingStream = fStreams.get(stream.getId());
606 if (existingStream != null) {
607 throw new ParseException("Stream id already exists"); //$NON-NLS-1$
608 }
609
610 /* This stream is valid and has a unique id. */
611 fStreams.put(stream.getId(), stream);
612 }
613
614 /**
615 * Gets the Environment variables from the trace metadata (See CTF spec)
616 *
617 * @return The environment variables in the form of an unmodifiable map
618 * (key, value)
619 */
620 public Map<String, String> getEnvironment() {
621 return Collections.unmodifiableMap(fEnvironment);
622 }
623
624 /**
625 * Add a variable to the environment variables
626 *
627 * @param varName
628 * the name of the variable
629 * @param varValue
630 * the value of the variable
631 */
632 public void addEnvironmentVar(String varName, String varValue) {
633 fEnvironment.put(varName, varValue);
634 }
635
636 /**
637 * Add a clock to the clock list
638 *
639 * @param nameValue
640 * the name of the clock (full name with scope)
641 * @param ctfClock
642 * the clock
643 */
644 public void addClock(String nameValue, CTFClock ctfClock) {
645 fClocks.put(nameValue, ctfClock);
646 }
647
648 /**
649 * gets the clock with a specific name
650 *
651 * @param name
652 * the name of the clock.
653 * @return the clock
654 */
655 public CTFClock getClock(String name) {
656 return fClocks.get(name);
657 }
658
659 /**
660 * gets the clock if there is only one. (this is 100% of the use cases as of
661 * June 2012)
662 *
663 * @return the clock
664 */
665 public final CTFClock getClock() {
666 if (fSingleClock != null && fClocks.size() == 1) {
667 return fSingleClock;
668 }
669 if (fClocks.size() == 1) {
670 fSingleClock = fClocks.get(fClocks.keySet().iterator().next());
671 return fSingleClock;
672 }
673 return null;
674 }
675
676 /**
677 * gets the time offset of a clock with respect to UTC in nanoseconds
678 *
679 * @return the time offset of a clock with respect to UTC in nanoseconds
680 */
681 public final long getOffset() {
682 if (getClock() == null) {
683 return 0;
684 }
685 return fSingleClock.getClockOffset();
686 }
687
688 /**
689 * gets the time offset of a clock with respect to UTC in nanoseconds
690 *
691 * @return the time offset of a clock with respect to UTC in nanoseconds
692 */
693 private double getTimeScale() {
694 if (getClock() == null) {
695 return 1.0;
696 }
697 return fSingleClock.getClockScale();
698 }
699
700 /**
701 * Gets the current first packet start time
702 *
703 * @return the current start time
704 */
705 public long getCurrentStartTime() {
706 long currentStart = Long.MAX_VALUE;
707 for (CTFStream stream : fStreams.values()) {
708 for (CTFStreamInput si : stream.getStreamInputs()) {
709 currentStart = Math.min(currentStart, si.getIndex().getElement(0).getTimestampBegin());
710 }
711 }
712 return timestampCyclesToNanos(currentStart);
713 }
714
715 /**
716 * Gets the current last packet end time
717 *
718 * @return the current end time
719 */
720 public long getCurrentEndTime() {
721 long currentEnd = Long.MIN_VALUE;
722 for (CTFStream stream : fStreams.values()) {
723 for (CTFStreamInput si : stream.getStreamInputs()) {
724 currentEnd = Math.max(currentEnd, si.getTimestampEnd());
725 }
726 }
727 return timestampCyclesToNanos(currentEnd);
728 }
729
730 /**
731 * Does the trace need to time scale?
732 *
733 * @return if the trace is in ns or cycles.
734 */
735 private boolean clockNeedsScale() {
736 if (getClock() == null) {
737 return false;
738 }
739 return fSingleClock.isClockScaled();
740 }
741
742 /**
743 * the inverse clock for returning to a scale.
744 *
745 * @return 1.0 / scale
746 */
747 private double getInverseTimeScale() {
748 if (getClock() == null) {
749 return 1.0;
750 }
751 return fSingleClock.getClockAntiScale();
752 }
753
754 /**
755 * @param cycles
756 * clock cycles since boot
757 * @return time in nanoseconds UTC offset
758 */
759 public long timestampCyclesToNanos(long cycles) {
760 long retVal = cycles + getOffset();
761 /*
762 * this fix is since quite often the offset will be > than 53 bits and
763 * therefore the conversion will be lossy
764 */
765 if (clockNeedsScale()) {
766 retVal = (long) (retVal * getTimeScale());
767 }
768 return retVal;
769 }
770
771 /**
772 * @param nanos
773 * time in nanoseconds UTC offset
774 * @return clock cycles since boot.
775 */
776 public long timestampNanoToCycles(long nanos) {
777 long retVal;
778 /*
779 * this fix is since quite often the offset will be > than 53 bits and
780 * therefore the conversion will be lossy
781 */
782 if (clockNeedsScale()) {
783 retVal = (long) (nanos * getInverseTimeScale());
784 } else {
785 retVal = nanos;
786 }
787 return retVal - getOffset();
788 }
789
790 /**
791 * Add a new stream
792 *
793 * @param id
794 * the ID of the stream
795 * @param streamFile
796 * new file in the stream
797 * @throws CTFException
798 * The file must exist
799 */
800 public void addStream(long id, File streamFile) throws CTFException {
801 CTFStream stream = null;
802 final File file = streamFile;
803 if (file == null) {
804 throw new CTFException("cannot create a stream with no file"); //$NON-NLS-1$
805 }
806 if (fStreams.containsKey(id)) {
807 stream = fStreams.get(id);
808 } else {
809 stream = new CTFStream(this);
810 fStreams.put(id, stream);
811 }
812 stream.addInput(new CTFStreamInput(stream, file));
813 }
814
815 /**
816 * Gets the current trace scope
817 *
818 * @return the current declaration scope
819 *
820 * @since 1.1
821 */
822 public DeclarationScope getScope() {
823 return fScope;
824 }
825 }
826
827 class MetadataFileFilter implements FileFilter {
828
829 @Override
830 public boolean accept(File pathname) {
831 if (pathname.isDirectory()) {
832 return false;
833 }
834 if (pathname.isHidden()) {
835 return false;
836 }
837 if (pathname.getName().equals("metadata")) { //$NON-NLS-1$
838 return false;
839 }
840 return true;
841 }
842
843 }
844
845 class MetadataComparator implements Comparator<File>, Serializable {
846
847 private static final long serialVersionUID = 1L;
848
849 @Override
850 public int compare(File o1, File o2) {
851 return o1.getName().compareTo(o2.getName());
852 }
853 }
This page took 0.055162 seconds and 5 git commands to generate.