Mercurial > jhg
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) |
