Skip to content

Commit 12edefb

Browse files
authored
Changed SAMRecord.toString() to emit the SAM format string with all fields (#1762)
* Changed SAMRecord.toString() to emit the SAM format string with all fields. * Drop sync + trailing newline from SAMRecord.getSAMString. * Remove deprecated SAMRecord.format() and now-dead helpers. The text formatting path used by SAMRecord.toString() / getSAMString() has been cleaned up: - SAMTextWriter.getSAMString no longer uses a JVM-wide synchronized static cache of a shared StringWriter/SAMTextWriter. It now allocates a fresh StringWriter per call. Without this change, every toString() call would have taken a global monitor. - SAMTextWriter.writeAlignment is split into a private writeAlignmentNoNewline plus a thin wrapper that appends '\n'. getSAMString uses the no-newline variant, so the result no longer needs to be trimmed. BREAKING CHANGE: SAMRecord.getSAMString() no longer terminates the returned String with a newline character. This brings SAMRecord into line with the other getSAMString() implementations (SAMSequenceRecord, SAMReadGroupRecord, SAMProgramRecord, SAMFileHeader), which already return newline-free strings. Callers that relied on the trailing '\n' as a separator (e.g. concatenating two records' SAM strings) must now insert their own separator. Callers that stripped the newline with .trim() can drop the call. BREAKING CHANGE: SAMRecord.format() has been removed. Callers should use SAMRecord.getSAMString() instead.
1 parent 6aa602f commit 12edefb

13 files changed

Lines changed: 72 additions & 134 deletions

src/main/java/htsjdk/samtools/DuplicateSetIterator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public DuplicateSet next() {
148148

149149
if (0 < cmp) {
150150
throw new SAMException("The input records were not sorted in duplicate order:\n" +
151-
representative.getSAMString() + record.getSAMString());
151+
representative.getSAMString() + "\n" + record.getSAMString());
152152
} else if (cmp < 0) {
153153
duplicateSet = this.duplicateSet;
154154
this.duplicateSet = new DuplicateSet(this.comparator);

src/main/java/htsjdk/samtools/SAMRecord.java

Lines changed: 4 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,82 +1840,6 @@ public int getAttributesBinarySize() {
18401840
return -1;
18411841
}
18421842

1843-
/**
1844-
*
1845-
* @return String representation of this.
1846-
* @deprecated This method is not guaranteed to return a valid SAM text representation of the SAMRecord.
1847-
* To get standard SAM text representation, {@link SAMRecord#getSAMString}.
1848-
*/
1849-
@Deprecated
1850-
public String format() {
1851-
final StringBuilder buffer = new StringBuilder();
1852-
addField(buffer, getReadName(), null, null);
1853-
addField(buffer, getFlags(), null, null);
1854-
addField(buffer, getReferenceName(), null, "*");
1855-
addField(buffer, getAlignmentStart(), 0, "*");
1856-
addField(buffer, getMappingQuality(), 0, "0");
1857-
addField(buffer, getCigarString(), null, "*");
1858-
addField(buffer, getMateReferenceName(), null, "*");
1859-
addField(buffer, getMateAlignmentStart(), 0, "*");
1860-
addField(buffer, getInferredInsertSize(), 0, "*");
1861-
addField(buffer, getReadString(), null, "*");
1862-
addField(buffer, getBaseQualityString(), null, "*");
1863-
if (mAttributes != null) {
1864-
SAMBinaryTagAndValue entry = getBinaryAttributes();
1865-
while (entry != null) {
1866-
addField(buffer, formatTagValue(entry.tag, entry.value));
1867-
entry = entry.getNext();
1868-
}
1869-
}
1870-
return buffer.toString();
1871-
}
1872-
1873-
private void addField(final StringBuilder buffer, final Object value, final Object defaultValue, final String defaultString) {
1874-
if (safeEquals(value, defaultValue)) {
1875-
addField(buffer, defaultString);
1876-
} else if (value == null) {
1877-
addField(buffer, "");
1878-
} else {
1879-
addField(buffer, value.toString());
1880-
}
1881-
}
1882-
1883-
private void addField(final StringBuilder buffer, final String field) {
1884-
if (buffer.length() > 0) {
1885-
buffer.append('\t');
1886-
}
1887-
buffer.append(field);
1888-
}
1889-
1890-
private static String formatTagValue(final short tag, final Object value) {
1891-
final String tagString = SAMTag.makeStringTag(tag);
1892-
if (value == null || value instanceof String) {
1893-
return tagString + ":Z:" + value;
1894-
} else if (value instanceof Integer || value instanceof Long ||
1895-
value instanceof Short || value instanceof Byte) {
1896-
return tagString + ":i:" + value;
1897-
} else if (value instanceof Character) {
1898-
return tagString + ":A:" + value;
1899-
} else if (value instanceof Float) {
1900-
return tagString + ":f:" + value;
1901-
} else if (value instanceof byte[]) {
1902-
return tagString + ":H:" + StringUtil.bytesToHexString((byte[]) value);
1903-
} else {
1904-
throw new RuntimeException("Unexpected value type for tag " + tagString +
1905-
": " + value + " of class " + value.getClass().getName());
1906-
}
1907-
}
1908-
1909-
private boolean safeEquals(final Object o1, final Object o2) {
1910-
if (o1 == o2) {
1911-
return true;
1912-
} else if (o1 == null || o2 == null) {
1913-
return false;
1914-
} else {
1915-
return o1.equals(o2);
1916-
}
1917-
}
1918-
19191843
/**
19201844
* Force all lazily-initialized data members to be initialized. If a subclass overrides this method,
19211845
* typically it should also call super method.
@@ -2386,37 +2310,15 @@ public SAMRecord deepCopy() {
23862310
return newSAM;
23872311
}
23882312

2389-
/** Simple toString() that gives a little bit of useful info about the read. */
2313+
/** Returns this record formatted as it would appear in a SAM file. */
23902314
@Override
23912315
public String toString() {
2392-
final StringBuilder builder = new StringBuilder(64);
2393-
builder.append(getReadName());
2394-
if (getReadPairedFlag()) {
2395-
if (getFirstOfPairFlag()) {
2396-
builder.append(" 1/2");
2397-
}
2398-
else {
2399-
builder.append(" 2/2");
2400-
}
2401-
}
2402-
2403-
builder.append(' ')
2404-
.append(String.valueOf(getReadLength()))
2405-
.append('b');
2406-
2407-
if (getReadUnmappedFlag()) {
2408-
builder.append(" unmapped read.");
2409-
}
2410-
else {
2411-
builder.append(String.format(" aligned to %s:%d-%d.", getContig(), getAlignmentStart(), getAlignmentEnd()));
2412-
}
2413-
2414-
return builder.toString();
2316+
return getSAMString();
24152317
}
24162318

24172319
/**
2418-
Returns the record in the SAM line-based text format. Fields are
2419-
separated by '\t' characters, and the String is terminated by '\n'.
2320+
Returns the record in the SAM line-based text format. Fields are separated by
2321+
'\t' characters. The returned String is NOT terminated by a newline.
24202322
*/
24212323
public String getSAMString() {
24222324
return SAMTextWriter.getSAMString(this);

src/main/java/htsjdk/samtools/SAMSortOrderChecker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public String getSortKey(final SAMRecord rec) {
7272
case queryname:
7373
return rec.getReadName();
7474
default:
75-
return rec.getSAMString().trim();
75+
return rec.getSAMString();
7676
}
7777
}
7878
}

src/main/java/htsjdk/samtools/SAMTextWriter.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ public SAMTextWriter(final OutputStream stream, final SamFlagField samFlagFieldO
123123
*/
124124
@Override
125125
public void writeAlignment(final SAMRecord alignment) {
126+
writeAlignmentNoNewline(alignment);
127+
try {
128+
out.write("\n");
129+
} catch (final IOException e) {
130+
throw new RuntimeIOException(e);
131+
}
132+
}
133+
134+
/** Writes the alignment fields without a trailing newline. Used by {@link #getSAMString}. */
135+
private void writeAlignmentNoNewline(final SAMRecord alignment) {
126136
try {
127137
out.write(alignment.getReadName());
128138
out.write(FIELD_SEPARATOR);
@@ -164,21 +174,15 @@ public void writeAlignment(final SAMRecord alignment) {
164174
out.write(encodedTag);
165175
attribute = attribute.getNext();
166176
}
167-
out.write("\n");
168-
169177
} catch (final IOException e) {
170178
throw new RuntimeIOException(e);
171179
}
172180
}
173181

174182
/* This method is called by SAMRecord.getSAMString(). */
175-
private static SAMTextWriter textWriter = null;
176-
private static StringWriter stringWriter = null;
177-
static synchronized String getSAMString(final SAMRecord alignment) {
178-
if (stringWriter == null) stringWriter = new StringWriter();
179-
if (textWriter == null) textWriter = new SAMTextWriter(stringWriter);
180-
stringWriter.getBuffer().setLength(0);
181-
textWriter.writeAlignment(alignment);
183+
static String getSAMString(final SAMRecord alignment) {
184+
final StringWriter stringWriter = new StringWriter();
185+
new SAMTextWriter(stringWriter).writeAlignmentNoNewline(alignment);
182186
return stringWriter.toString();
183187
}
184188

src/main/java/htsjdk/samtools/SamReader.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,8 @@ public SAMRecord next() {
594594
if (!checker.isSorted(result)) {
595595
throw new IllegalStateException(String.format(
596596
"Record %s should come after %s when sorting with %s ordering.",
597-
previous.getSAMString().trim(),
598-
result.getSAMString().trim(), checker.getSortOrder()));
597+
previous.getSAMString(),
598+
result.getSAMString(), checker.getSortOrder()));
599599
}
600600
}
601601
return result;

src/main/java/htsjdk/samtools/cram/CramComparison.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ private static int compareFiles(final SamReaderFactory factory, final String fil
131131
diffCount++;
132132
if (diffCount <= maxDiffs) {
133133
emit(out, "Record %d: %s", recordCount, diff);
134-
emit(out, " file1: %s", rec1.getSAMString().trim());
135-
emit(out, " file2: %s", rec2.getSAMString().trim());
134+
emit(out, " file1: %s", rec1.getSAMString());
135+
emit(out, " file2: %s", rec2.getSAMString());
136136
}
137137
}
138138
}

src/main/java/htsjdk/samtools/sra/SRALazyRecord.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -725,14 +725,6 @@ public Object clone() throws CloneNotSupportedException {
725725
return newObject;
726726
}
727727

728-
@Override
729-
public String format() {
730-
if (!initializedAttributes.contains(LazyAttribute.RG)) {
731-
getAttribute("RG");
732-
}
733-
return super.format();
734-
}
735-
736728
@Override
737729
public List<SAMValidationError> isValid(final boolean firstOnly) {
738730
loadFields();

src/test/java/htsjdk/samtools/BAMFileIndexTest.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,6 @@ private int runQueryTest(final File bamFile, final String sequence, final int st
418418
record2 = iter2.next();
419419
count2++;
420420
}
421-
// System.out.println("Iteration:");
422-
// System.out.println(" Record1 = " + ((record1 == null) ? "null" : record1.format()));
423-
// System.out.println(" Record2 = " + ((record2 == null) ? "null" : record2.format()));
424421
if (record1 == null && record2 == null) {
425422
break;
426423
}

src/test/java/htsjdk/samtools/BAMRemoteFileTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ private void runLocalRemoteTest(final URL bamURL, final File bamFile, final Stri
169169
assertTrue(records1.size() > 0);
170170
assertEquals(records1.size(), records2.size());
171171
for (int i = 0; i < records1.size(); i++) {
172-
//System.out.println(records1.get(i).format());
173172
assertEquals(records1.get(i).getSAMString(), records2.get(i).getSAMString());
174173
}
175174
}

src/test/java/htsjdk/samtools/SAMRecordUnitTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,4 +1205,48 @@ public void testAlignmentBlockCacheInvalidation() {
12051205
rec.setAlignmentStart(100);
12061206
Assert.assertEquals(100, rec.getAlignmentBlocks().get(0).getReferenceStart());
12071207
}
1208+
1209+
@Test
1210+
public void testToStringReturnsSamLine() {
1211+
final SAMRecord rec = createTestRecordHelper();
1212+
final String[] fields = rec.toString().split("\t", -1);
1213+
Assert.assertTrue(fields.length >= 11, "Expected at least 11 mandatory SAM fields, got " + fields.length);
1214+
Assert.assertEquals(fields[0], rec.getReadName());
1215+
Assert.assertEquals(fields[1], Integer.toString(rec.getFlags()));
1216+
Assert.assertEquals(fields[2], rec.getReferenceName());
1217+
Assert.assertEquals(fields[3], Integer.toString(rec.getAlignmentStart()));
1218+
Assert.assertEquals(fields[5], rec.getCigarString());
1219+
}
1220+
1221+
@Test
1222+
public void testToStringHasNoTrailingNewline() {
1223+
final SAMRecord rec = createTestRecordHelper();
1224+
final String s = rec.toString();
1225+
Assert.assertFalse(s.isEmpty());
1226+
Assert.assertNotEquals(s.charAt(s.length() - 1), '\n');
1227+
}
1228+
1229+
@Test
1230+
public void testGetSAMStringHasNoTrailingNewline() {
1231+
final SAMRecord rec = createTestRecordHelper();
1232+
final String s = rec.getSAMString();
1233+
Assert.assertFalse(s.isEmpty());
1234+
Assert.assertNotEquals(s.charAt(s.length() - 1), '\n');
1235+
}
1236+
1237+
@Test
1238+
public void testToStringEqualsGetSAMString() {
1239+
final SAMRecord rec = createTestRecordHelper();
1240+
Assert.assertEquals(rec.toString(), rec.getSAMString());
1241+
}
1242+
1243+
@Test
1244+
public void testToStringIncludesAttributes() {
1245+
final SAMRecord rec = createTestRecordHelper();
1246+
rec.setAttribute("XX", 42);
1247+
rec.setAttribute("YY", "hello");
1248+
final String s = rec.toString();
1249+
Assert.assertTrue(s.contains("XX:i:42"), "Expected XX:i:42 in: " + s);
1250+
Assert.assertTrue(s.contains("YY:Z:hello"), "Expected YY:Z:hello in: " + s);
1251+
}
12081252
}

0 commit comments

Comments
 (0)