2ef8fec142e60aa8f98e41a5c995974657660443
[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 CTFStream stream = fStreams.get(streamId);
225 if (stream == null) {
226 return null;
227 }
228 return stream.getEventDeclarations();
229 }
230
231 /**
232 * Method getStream gets the stream for a given id
233 *
234 * @param id
235 * Long the id of the stream
236 * @return Stream the stream that we need
237 */
238 public CTFStream getStream(Long id) {
239 if (id == null) {
240 return fStreams.get(0L);
241 }
242 return fStreams.get(id);
243 }
244
245 /**
246 * Method nbStreams gets the number of available streams
247 *
248 * @return int the number of streams
249 */
250 public int nbStreams() {
251 return fStreams.size();
252 }
253
254 /**
255 * Method setMajor sets the major version of the trace (DO NOT USE)
256 *
257 * @param major
258 * long the major version
259 */
260 public void setMajor(long major) {
261 fMajor = major;
262 }
263
264 /**
265 * Method setMinor sets the minor version of the trace (DO NOT USE)
266 *
267 * @param minor
268 * long the minor version
269 */
270 public void setMinor(long minor) {
271 fMinor = minor;
272 }
273
274 /**
275 * Method setUUID sets the UUID of a trace
276 *
277 * @param uuid
278 * UUID
279 */
280 public void setUUID(UUID uuid) {
281 fUuid = uuid;
282 }
283
284 /**
285 * Method setByteOrder sets the byte order
286 *
287 * @param byteOrder
288 * ByteOrder of the trace, can be little-endian or big-endian
289 */
290 public void setByteOrder(ByteOrder byteOrder) {
291 fByteOrder = byteOrder;
292 }
293
294 /**
295 * Method setPacketHeader sets the packet header of a trace (DO NOT USE)
296 *
297 * @param packetHeader
298 * StructDeclaration the header in structdeclaration form
299 */
300 public void setPacketHeader(StructDeclaration packetHeader) {
301 fPacketHeaderDecl = packetHeader;
302 }
303
304 /**
305 * Method majorIsSet is the major version number set?
306 *
307 * @return boolean is the major set?
308 */
309 public boolean majorIsSet() {
310 return fMajor != null;
311 }
312
313 /**
314 * Method minorIsSet. is the minor version number set?
315 *
316 * @return boolean is the minor set?
317 */
318 public boolean minorIsSet() {
319 return fMinor != null;
320 }
321
322 /**
323 * Method UUIDIsSet is the UUID set?
324 *
325 * @return boolean is the UUID set?
326 */
327 public boolean uuidIsSet() {
328 return fUuid != null;
329 }
330
331 /**
332 * Method byteOrderIsSet is the byteorder set?
333 *
334 * @return boolean is the byteorder set?
335 */
336 public boolean byteOrderIsSet() {
337 return fByteOrder != null;
338 }
339
340 /**
341 * Method packetHeaderIsSet is the packet header set?
342 *
343 * @return boolean is the packet header set?
344 */
345 public boolean packetHeaderIsSet() {
346 return fPacketHeaderDecl != null;
347 }
348
349 /**
350 * Method getUUID gets the trace UUID
351 *
352 * @return UUID gets the trace UUID
353 */
354 public UUID getUUID() {
355 return fUuid;
356 }
357
358 /**
359 * Method getMajor gets the trace major version
360 *
361 * @return long gets the trace major version
362 */
363 public long getMajor() {
364 return fMajor;
365 }
366
367 /**
368 * Method getMinor gets the trace minor version
369 *
370 * @return long gets the trace minor version
371 */
372 public long getMinor() {
373 return fMinor;
374 }
375
376 /**
377 * Method getByteOrder gets the trace byte order
378 *
379 * @return ByteOrder gets the trace byte order
380 */
381 public final ByteOrder getByteOrder() {
382 return fByteOrder;
383 }
384
385 /**
386 * Method getPacketHeader gets the trace packet header
387 *
388 * @return StructDeclaration gets the trace packet header
389 */
390 public StructDeclaration getPacketHeader() {
391 return fPacketHeaderDecl;
392 }
393
394 /**
395 * Method getTraceDirectory gets the trace directory
396 *
397 * @return File the path in "File" format.
398 */
399 public File getTraceDirectory() {
400 return fPath;
401 }
402
403 /**
404 * Get all the streams as an iterable.
405 *
406 * @return Iterable&lt;Stream&gt; an iterable over streams.
407 */
408 public Iterable<CTFStream> getStreams() {
409 return fStreams.values();
410 }
411
412 /**
413 * Method getPath gets the path of the trace directory
414 *
415 * @return String the path of the trace directory, in string format.
416 * @see java.io.File#getPath()
417 */
418 public String getPath() {
419 return (fPath != null) ? fPath.getPath() : ""; //$NON-NLS-1$
420 }
421
422 // ------------------------------------------------------------------------
423 // Operations
424 // ------------------------------------------------------------------------
425
426 private void addStream(CTFStreamInput s) {
427
428 /*
429 * add the stream
430 */
431 CTFStream stream = s.getStream();
432 fStreams.put(stream.getId(), stream);
433
434 /*
435 * index the trace
436 */
437 s.setupIndex();
438 }
439
440 /**
441 * Tries to open the given file, reads the first packet header of the file
442 * and check its validity. This will add a file to a stream as a streaminput
443 *
444 * @param streamFile
445 * A trace file in the trace directory.
446 * @param index
447 * Which index in the class' streamFileChannel array this file
448 * must use
449 * @throws CTFException
450 * if there is a file error
451 */
452 private CTFStream openStreamInput(File streamFile) throws CTFException {
453 ByteBuffer byteBuffer;
454 BitBuffer streamBitBuffer;
455 CTFStream stream;
456
457 if (!streamFile.canRead()) {
458 throw new CTFException("Unreadable file : " //$NON-NLS-1$
459 + streamFile.getPath());
460 }
461 if (streamFile.length() == 0) {
462 return null;
463 }
464 try (FileChannel fc = FileChannel.open(streamFile.toPath(), StandardOpenOption.READ)) {
465 /* Map one memory page of 4 kiB */
466 byteBuffer = SafeMappedByteBuffer.map(fc, MapMode.READ_ONLY, 0, (int) Math.min(fc.size(), 4096L));
467 if (byteBuffer == null) {
468 throw new IllegalStateException("Failed to allocate memory"); //$NON-NLS-1$
469 }
470 /* Create a BitBuffer with this mapping and the trace byte order */
471 streamBitBuffer = new BitBuffer(byteBuffer, this.getByteOrder());
472 if (fPacketHeaderDecl != null) {
473 /* Read the packet header */
474 fPacketHeaderDef = fPacketHeaderDecl.createDefinition(this, ILexicalScope.PACKET_HEADER, streamBitBuffer);
475 }
476 } catch (IOException e) {
477 /* Shouldn't happen at this stage if every other check passed */
478 throw new CTFException(e);
479 }
480 if (fPacketHeaderDef != null) {
481 validateMagicNumber(fPacketHeaderDef);
482
483 validateUUID(fPacketHeaderDef);
484
485 /* Read the stream ID */
486 IDefinition streamIDDef = fPacketHeaderDef.lookupDefinition("stream_id"); //$NON-NLS-1$
487
488 if (streamIDDef instanceof IntegerDefinition) {
489 /* This doubles as a null check */
490 long streamID = ((IntegerDefinition) streamIDDef).getValue();
491 stream = fStreams.get(streamID);
492 } else {
493 /* No stream_id in the packet header */
494 stream = getStream(null);
495 }
496
497 } else {
498 /* No packet header, we suppose there is only one stream */
499 stream = getStream(null);
500 }
501
502 if (stream == null) {
503 throw new CTFException("Unexpected end of stream"); //$NON-NLS-1$
504 }
505
506 /*
507 * Create the stream input and add a reference to the streamInput in the
508 * stream.
509 */
510 stream.addInput(new CTFStreamInput(stream, streamFile));
511
512 return stream;
513 }
514
515 private void validateUUID(StructDefinition packetHeaderDef) throws CTFException {
516 IDefinition lookupDefinition = packetHeaderDef.lookupDefinition("uuid"); //$NON-NLS-1$
517 AbstractArrayDefinition uuidDef = (AbstractArrayDefinition) lookupDefinition;
518 if (uuidDef != null) {
519 UUID otheruuid = Utils.getUUIDfromDefinition(uuidDef);
520 if (!fUuid.equals(otheruuid)) {
521 throw new CTFException("UUID mismatch"); //$NON-NLS-1$
522 }
523 }
524 }
525
526 private static void validateMagicNumber(StructDefinition packetHeaderDef) throws CTFException {
527 IntegerDefinition magicDef = (IntegerDefinition) packetHeaderDef.lookupDefinition(CTFStrings.MAGIC);
528 if (magicDef != null) {
529 int magic = (int) magicDef.getValue();
530 if (magic != Utils.CTF_MAGIC) {
531 throw new CTFException("CTF magic mismatch"); //$NON-NLS-1$
532 }
533 }
534 }
535
536 // ------------------------------------------------------------------------
537 // IDefinitionScope
538 // ------------------------------------------------------------------------
539
540 /**
541 * @since 1.0
542 */
543 @Override
544 public ILexicalScope getScopePath() {
545 return ILexicalScope.TRACE;
546 }
547
548 /**
549 * Looks up a definition from packet
550 *
551 * @param lookupPath
552 * String
553 * @return Definition
554 * @see org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope#lookupDefinition(String)
555 */
556 @Override
557 public Definition lookupDefinition(String lookupPath) {
558 if (lookupPath.equals(ILexicalScope.TRACE_PACKET_HEADER.getPath())) {
559 return fPacketHeaderDef;
560 }
561 return null;
562 }
563
564 // ------------------------------------------------------------------------
565 // Live trace reading
566 // ------------------------------------------------------------------------
567
568 /**
569 * Add a new stream file to support new streams while the trace is being
570 * read.
571 *
572 * @param streamFile
573 * the file of the stream
574 * @throws CTFException
575 * A stream had an issue being read
576 */
577 public void addStreamFile(File streamFile) throws CTFException {
578 openStreamInput(streamFile);
579 }
580
581 /**
582 * Registers a new stream to the trace.
583 *
584 * @param stream
585 * A stream object.
586 * @throws ParseException
587 * If there was some problem reading the metadata
588 */
589 public void addStream(CTFStream stream) throws ParseException {
590 /*
591 * If there is already a stream without id (the null key), it must be
592 * the only one
593 */
594 if (fStreams.get(null) != null) {
595 throw new ParseException("Stream without id with multiple streams"); //$NON-NLS-1$
596 }
597
598 /*
599 * If the stream we try to add has no key set, it must be the only one.
600 * Thus, if the streams container is not empty, it is not valid.
601 */
602 if ((!stream.isIdSet()) && (!fStreams.isEmpty())) {
603 throw new ParseException("Stream without id with multiple streams"); //$NON-NLS-1$
604 }
605
606 /*
607 * If a stream with the same ID already exists, it is not valid.
608 */
609 CTFStream existingStream = fStreams.get(stream.getId());
610 if (existingStream != null) {
611 throw new ParseException("Stream id already exists"); //$NON-NLS-1$
612 }
613
614 /* This stream is valid and has a unique id. */
615 fStreams.put(stream.getId(), stream);
616 }
617
618 /**
619 * Gets the Environment variables from the trace metadata (See CTF spec)
620 *
621 * @return The environment variables in the form of an unmodifiable map
622 * (key, value)
623 */
624 public Map<String, String> getEnvironment() {
625 return Collections.unmodifiableMap(fEnvironment);
626 }
627
628 /**
629 * Add a variable to the environment variables
630 *
631 * @param varName
632 * the name of the variable
633 * @param varValue
634 * the value of the variable
635 */
636 public void addEnvironmentVar(String varName, String varValue) {
637 fEnvironment.put(varName, varValue);
638 }
639
640 /**
641 * Add a clock to the clock list
642 *
643 * @param nameValue
644 * the name of the clock (full name with scope)
645 * @param ctfClock
646 * the clock
647 */
648 public void addClock(String nameValue, CTFClock ctfClock) {
649 fClocks.put(nameValue, ctfClock);
650 }
651
652 /**
653 * gets the clock with a specific name
654 *
655 * @param name
656 * the name of the clock.
657 * @return the clock
658 */
659 public CTFClock getClock(String name) {
660 return fClocks.get(name);
661 }
662
663 /**
664 * gets the clock if there is only one. (this is 100% of the use cases as of
665 * June 2012)
666 *
667 * @return the clock
668 */
669 public final CTFClock getClock() {
670 if (fSingleClock != null && fClocks.size() == 1) {
671 return fSingleClock;
672 }
673 if (fClocks.size() == 1) {
674 fSingleClock = fClocks.get(fClocks.keySet().iterator().next());
675 return fSingleClock;
676 }
677 return null;
678 }
679
680 /**
681 * gets the time offset of a clock with respect to UTC in nanoseconds
682 *
683 * @return the time offset of a clock with respect to UTC in nanoseconds
684 */
685 public final long getOffset() {
686 if (getClock() == null) {
687 return 0;
688 }
689 return fSingleClock.getClockOffset();
690 }
691
692 /**
693 * gets the time offset of a clock with respect to UTC in nanoseconds
694 *
695 * @return the time offset of a clock with respect to UTC in nanoseconds
696 */
697 private double getTimeScale() {
698 if (getClock() == null) {
699 return 1.0;
700 }
701 return fSingleClock.getClockScale();
702 }
703
704 /**
705 * Gets the current first packet start time
706 *
707 * @return the current start time
708 */
709 public long getCurrentStartTime() {
710 long currentStart = Long.MAX_VALUE;
711 for (CTFStream stream : fStreams.values()) {
712 for (CTFStreamInput si : stream.getStreamInputs()) {
713 currentStart = Math.min(currentStart, si.getIndex().getElement(0).getTimestampBegin());
714 }
715 }
716 return timestampCyclesToNanos(currentStart);
717 }
718
719 /**
720 * Gets the current last packet end time
721 *
722 * @return the current end time
723 */
724 public long getCurrentEndTime() {
725 long currentEnd = Long.MIN_VALUE;
726 for (CTFStream stream : fStreams.values()) {
727 for (CTFStreamInput si : stream.getStreamInputs()) {
728 currentEnd = Math.max(currentEnd, si.getTimestampEnd());
729 }
730 }
731 return timestampCyclesToNanos(currentEnd);
732 }
733
734 /**
735 * Does the trace need to time scale?
736 *
737 * @return if the trace is in ns or cycles.
738 */
739 private boolean clockNeedsScale() {
740 if (getClock() == null) {
741 return false;
742 }
743 return fSingleClock.isClockScaled();
744 }
745
746 /**
747 * the inverse clock for returning to a scale.
748 *
749 * @return 1.0 / scale
750 */
751 private double getInverseTimeScale() {
752 if (getClock() == null) {
753 return 1.0;
754 }
755 return fSingleClock.getClockAntiScale();
756 }
757
758 /**
759 * @param cycles
760 * clock cycles since boot
761 * @return time in nanoseconds UTC offset
762 */
763 public long timestampCyclesToNanos(long cycles) {
764 long retVal = cycles + getOffset();
765 /*
766 * this fix is since quite often the offset will be > than 53 bits and
767 * therefore the conversion will be lossy
768 */
769 if (clockNeedsScale()) {
770 retVal = (long) (retVal * getTimeScale());
771 }
772 return retVal;
773 }
774
775 /**
776 * @param nanos
777 * time in nanoseconds UTC offset
778 * @return clock cycles since boot.
779 */
780 public long timestampNanoToCycles(long nanos) {
781 long retVal;
782 /*
783 * this fix is since quite often the offset will be > than 53 bits and
784 * therefore the conversion will be lossy
785 */
786 if (clockNeedsScale()) {
787 retVal = (long) (nanos * getInverseTimeScale());
788 } else {
789 retVal = nanos;
790 }
791 return retVal - getOffset();
792 }
793
794 /**
795 * Add a new stream
796 *
797 * @param id
798 * the ID of the stream
799 * @param streamFile
800 * new file in the stream
801 * @throws CTFException
802 * The file must exist
803 */
804 public void addStream(long id, File streamFile) throws CTFException {
805 final File file = streamFile;
806 if (file == null) {
807 throw new CTFException("cannot create a stream with no file"); //$NON-NLS-1$
808 }
809 CTFStream stream = fStreams.get(id);
810 if (stream == null) {
811 stream = new CTFStream(this);
812 fStreams.put(id, stream);
813 }
814 stream.addInput(new CTFStreamInput(stream, file));
815 }
816
817 /**
818 * Gets the current trace scope
819 *
820 * @return the current declaration scope
821 *
822 * @since 1.1
823 */
824 public DeclarationScope getScope() {
825 return fScope;
826 }
827 }
828
829 class MetadataFileFilter implements FileFilter {
830
831 @Override
832 public boolean accept(File pathname) {
833 if (pathname.isDirectory()) {
834 return false;
835 }
836 if (pathname.isHidden()) {
837 return false;
838 }
839 if (pathname.getName().equals("metadata")) { //$NON-NLS-1$
840 return false;
841 }
842 return true;
843 }
844
845 }
846
847 class MetadataComparator implements Comparator<File>, Serializable {
848
849 private static final long serialVersionUID = 1L;
850
851 @Override
852 public int compare(File o1, File o2) {
853 return o1.getName().compareTo(o2.getName());
854 }
855 }
This page took 0.055304 seconds and 5 git commands to generate.