comparison src/org/tmatesoft/hg/repo/HgDataFile.java @ 602:e3717fc7d26f

Refactor metadata parsing in HgDataFile, moved to standalone class
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Mon, 06 May 2013 17:11:29 +0200
parents 46f29b73e51e
children 707b5c7c6fa4
comparison
equal deleted inserted replaced
601:8143c1f77d45 602:e3717fc7d26f
16 */ 16 */
17 package org.tmatesoft.hg.repo; 17 package org.tmatesoft.hg.repo;
18 18
19 import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex; 19 import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex;
20 import static org.tmatesoft.hg.repo.HgRepository.*; 20 import static org.tmatesoft.hg.repo.HgRepository.*;
21 import static org.tmatesoft.hg.util.LogFacility.Severity.*; 21 import static org.tmatesoft.hg.util.LogFacility.Severity.Info;
22 22 import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;
23 import java.io.ByteArrayOutputStream; 23
24 import java.io.File; 24 import java.io.File;
25 import java.io.FileInputStream; 25 import java.io.FileInputStream;
26 import java.io.IOException; 26 import java.io.IOException;
27 import java.nio.ByteBuffer; 27 import java.nio.ByteBuffer;
28 import java.nio.channels.FileChannel; 28 import java.nio.channels.FileChannel;
29 import java.util.ArrayList;
30 import java.util.Arrays; 29 import java.util.Arrays;
31 import java.util.Collection;
32 30
33 import org.tmatesoft.hg.core.HgChangesetFileSneaker; 31 import org.tmatesoft.hg.core.HgChangesetFileSneaker;
34 import org.tmatesoft.hg.core.Nodeid; 32 import org.tmatesoft.hg.core.Nodeid;
35 import org.tmatesoft.hg.internal.DataAccess; 33 import org.tmatesoft.hg.internal.DataAccess;
36 import org.tmatesoft.hg.internal.FilterByteChannel; 34 import org.tmatesoft.hg.internal.FilterByteChannel;
37 import org.tmatesoft.hg.internal.FilterDataAccess; 35 import org.tmatesoft.hg.internal.FilterDataAccess;
38 import org.tmatesoft.hg.internal.IntMap;
39 import org.tmatesoft.hg.internal.Internals; 36 import org.tmatesoft.hg.internal.Internals;
37 import org.tmatesoft.hg.internal.Metadata;
40 import org.tmatesoft.hg.internal.RevlogStream; 38 import org.tmatesoft.hg.internal.RevlogStream;
41 import org.tmatesoft.hg.util.ByteChannel; 39 import org.tmatesoft.hg.util.ByteChannel;
42 import org.tmatesoft.hg.util.CancelSupport; 40 import org.tmatesoft.hg.util.CancelSupport;
43 import org.tmatesoft.hg.util.CancelledException; 41 import org.tmatesoft.hg.util.CancelledException;
44 import org.tmatesoft.hg.util.LogFacility; 42 import org.tmatesoft.hg.util.LogFacility;
277 } 275 }
278 if (sink == null) { 276 if (sink == null) {
279 throw new IllegalArgumentException(); 277 throw new IllegalArgumentException();
280 } 278 }
281 if (metadata == null) { 279 if (metadata == null) {
282 metadata = new Metadata(); 280 metadata = new Metadata(getRepo());
283 } 281 }
284 ErrorHandlingInspector insp; 282 ErrorHandlingInspector insp;
285 final LogFacility lf = getRepo().getSessionContext().getLog(); 283 final LogFacility lf = getRepo().getSessionContext().getLog();
286 if (metadata.none(fileRevisionIndex)) { 284 if (metadata.none(fileRevisionIndex)) {
287 insp = new ContentPipe(sink, 0, lf); 285 insp = new ContentPipe(sink, 0, lf);
288 } else if (metadata.known(fileRevisionIndex)) { 286 } else if (metadata.known(fileRevisionIndex)) {
289 insp = new ContentPipe(sink, metadata.dataOffset(fileRevisionIndex), lf); 287 insp = new ContentPipe(sink, metadata.dataOffset(fileRevisionIndex), lf);
290 } else { 288 } else {
291 // do not know if there's metadata 289 // do not know if there's metadata
292 insp = new MetadataInspector(metadata, lf, new ContentPipe(sink, 0, lf)); 290 insp = new MetadataInspector(metadata, new ContentPipe(sink, 0, lf));
293 } 291 }
294 insp.checkCancelled(); 292 insp.checkCancelled();
295 super.content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp); 293 super.content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp);
296 try { 294 try {
297 insp.checkFailed(); 295 insp.checkFailed();
439 return sb.toString(); 437 return sb.toString();
440 } 438 }
441 439
442 private void checkAndRecordMetadata(int localRev) throws HgInvalidControlFileException { 440 private void checkAndRecordMetadata(int localRev) throws HgInvalidControlFileException {
443 if (metadata == null) { 441 if (metadata == null) {
444 metadata = new Metadata(); 442 metadata = new Metadata(getRepo());
445 } 443 }
446 // use MetadataInspector without delegate to process metadata only 444 // use MetadataInspector without delegate to process metadata only
447 RevlogStream.Inspector insp = new MetadataInspector(metadata, getRepo().getSessionContext().getLog(), null); 445 RevlogStream.Inspector insp = new MetadataInspector(metadata, null);
448 super.content.iterate(localRev, localRev, true, insp); 446 super.content.iterate(localRev, localRev, true, insp);
449 }
450
451 private static final class MetadataEntry {
452 private final String entry;
453 private final int valueStart;
454
455 // key may be null
456 /*package-local*/MetadataEntry(String key, String value) {
457 if (key == null) {
458 entry = value;
459 valueStart = -1; // not 0 to tell between key == null and key == ""
460 } else {
461 entry = key + value;
462 valueStart = key.length();
463 }
464 }
465 /*package-local*/boolean matchKey(String key) {
466 return key == null ? valueStart == -1 : key.length() == valueStart && entry.startsWith(key);
467 }
468 // uncomment once/if needed
469 // public String key() {
470 // return entry.substring(0, valueStart);
471 // }
472 public String value() {
473 return valueStart == -1 ? entry : entry.substring(valueStart);
474 }
475 }
476
477 private static class Metadata {
478 private static class Record {
479 public final int offset;
480 public final MetadataEntry[] entries;
481
482 public Record(int off, MetadataEntry[] entr) {
483 offset = off;
484 entries = entr;
485 }
486 }
487 // XXX sparse array needed
488 private final IntMap<Record> entries = new IntMap<Record>(5);
489
490 private final Record NONE = new Record(-1, null); // don't want statics
491
492 // true when there's metadata for given revision
493 boolean known(int revision) {
494 Record i = entries.get(revision);
495 return i != null && NONE != i;
496 }
497
498 // true when revision has been checked for metadata presence.
499 public boolean checked(int revision) {
500 return entries.containsKey(revision);
501 }
502
503 // true when revision has been checked and found not having any metadata
504 boolean none(int revision) {
505 Record i = entries.get(revision);
506 return i == NONE;
507 }
508
509 // mark revision as having no metadata.
510 void recordNone(int revision) {
511 Record i = entries.get(revision);
512 if (i == NONE) {
513 return; // already there
514 }
515 if (i != null) {
516 throw new IllegalStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i));
517 }
518 entries.put(revision, NONE);
519 }
520
521 // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before)
522 int dataOffset(int revision) {
523 return entries.get(revision).offset;
524 }
525 void add(int revision, int dataOffset, Collection<MetadataEntry> e) {
526 assert !entries.containsKey(revision);
527 entries.put(revision, new Record(dataOffset, e.toArray(new MetadataEntry[e.size()])));
528 }
529
530 String find(int revision, String key) {
531 for (MetadataEntry me : entries.get(revision).entries) {
532 if (me.matchKey(key)) {
533 return me.value();
534 }
535 }
536 return null;
537 }
538 } 447 }
539 448
540 private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector { 449 private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector {
541 private final Metadata metadata; 450 private final Metadata metadata;
542 private final RevlogStream.Inspector delegate; 451 private final RevlogStream.Inspector delegate;
543 private final LogFacility log;
544 452
545 /** 453 /**
546 * @param _metadata never <code>null</code> 454 * @param _metadata never <code>null</code>
547 * @param logFacility log facility from the session context
548 * @param chain <code>null</code> if no further data processing other than metadata is desired 455 * @param chain <code>null</code> if no further data processing other than metadata is desired
549 */ 456 */
550 public MetadataInspector(Metadata _metadata, LogFacility logFacility, RevlogStream.Inspector chain) { 457 public MetadataInspector(Metadata _metadata, RevlogStream.Inspector chain) {
551 metadata = _metadata; 458 metadata = _metadata;
552 log = logFacility;
553 delegate = chain; 459 delegate = chain;
554 setCancelSupport(CancelSupport.Factory.get(chain)); 460 setCancelSupport(CancelSupport.Factory.get(chain));
555 } 461 }
556 462
557 public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { 463 public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) {
558 try { 464 try {
559 final int daLength = data.length(); 465 if (metadata.tryRead(revisionNumber, data)) {
560 if (daLength < 4 || data.readByte() != 1 || data.readByte() != 10) {
561 metadata.recordNone(revisionNumber);
562 data.reset();
563 } else {
564 ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>();
565 int offset = parseMetadata(data, daLength, _metadata);
566 metadata.add(revisionNumber, offset, _metadata);
567 // da is in prepared state (i.e. we consumed all bytes up to metadata end). 466 // da is in prepared state (i.e. we consumed all bytes up to metadata end).
568 // However, it's not safe to assume delegate won't call da.reset() for some reason, 467 // However, it's not safe to assume delegate won't call da.reset() for some reason,
569 // and we need to ensure predictable result. 468 // and we need to ensure predictable result.
570 data.reset(); 469 data.reset();
571 data = new FilterDataAccess(data, offset, daLength - offset); 470 int offset = metadata.dataOffset(revisionNumber);
471 data = new FilterDataAccess(data, offset, data.length() - offset);
472 } else {
473 data.reset();
572 } 474 }
573 if (delegate != null) { 475 if (delegate != null) {
574 delegate.next(revisionNumber, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeid, data); 476 delegate.next(revisionNumber, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeid, data);
575 } 477 }
576 } catch (IOException ex) { 478 } catch (IOException ex) {
579 // TODO RevlogStream, where this RevlogStream.Inspector goes, shall set File (as it's the only one having access to it) 481 // TODO RevlogStream, where this RevlogStream.Inspector goes, shall set File (as it's the only one having access to it)
580 recordFailure(ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionNumber)); 482 recordFailure(ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionNumber));
581 } 483 }
582 } 484 }
583 485
584 private int parseMetadata(DataAccess data, final int daLength, ArrayList<MetadataEntry> _metadata) throws IOException, HgInvalidControlFileException {
585 int lastEntryStart = 2;
586 int lastColon = -1;
587 // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder,
588 // which can't be used here because we can't convert bytes to chars as we read them
589 // (there might be multi-byte encoding), and we need to collect all bytes before converting to string
590 ByteArrayOutputStream bos = new ByteArrayOutputStream();
591 String key = null, value = null;
592 boolean byteOne = false;
593 boolean metadataIsComplete = false;
594 for (int i = 2; i < daLength; i++) {
595 byte b = data.readByte();
596 if (b == '\n') {
597 if (byteOne) { // i.e. \n follows 1
598 lastEntryStart = i+1;
599 metadataIsComplete = true;
600 // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n)
601 // if yes, need to set metadataIsComplete to true in that case as well
602 break;
603 }
604 if (key == null || lastColon == -1 || i <= lastColon) {
605 log.dump(getClass(), Error, "Missing key in file revision metadata at index %d", i);
606 }
607 value = new String(bos.toByteArray()).trim();
608 bos.reset();
609 _metadata.add(new MetadataEntry(key, value));
610 key = value = null;
611 lastColon = -1;
612 lastEntryStart = i+1;
613 continue;
614 }
615 // byteOne has to be consumed up to this line, if not yet, consume it
616 if (byteOne) {
617 // insert 1 we've read on previous step into the byte builder
618 bos.write(1);
619 byteOne = false;
620 // fall-through to consume current byte
621 }
622 if (b == (int) ':') {
623 assert value == null;
624 key = new String(bos.toByteArray());
625 bos.reset();
626 lastColon = i;
627 } else if (b == 1) {
628 byteOne = true;
629 } else {
630 bos.write(b);
631 }
632 }
633 // data.isEmpty is not reliable, renamed files of size==0 keep only metadata
634 if (!metadataIsComplete) {
635 // XXX perhaps, worth a testcase (empty file, renamed, read or ask ifCopy
636 throw new HgInvalidControlFileException("Metadata is not closed properly", null, null);
637 }
638 return lastEntryStart;
639 }
640
641 @Override 486 @Override
642 public void checkFailed() throws HgRuntimeException, IOException, CancelledException { 487 public void checkFailed() throws HgRuntimeException, IOException, CancelledException {
643 super.checkFailed(); 488 super.checkFailed();
644 if (delegate instanceof ErrorHandlingInspector) { 489 if (delegate instanceof ErrorHandlingInspector) {
645 // TODO need to add ErrorDestination (ErrorTarget/Acceptor?) and pass it around (much like CancelSupport get passed) 490 // TODO need to add ErrorDestination (ErrorTarget/Acceptor?) and pass it around (much like CancelSupport get passed)