/*
* The MIT License (MIT)
*
* Copyright (c) 2007-2015 Broad Institute
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.broad.igv.sam.reader;
import htsjdk.samtools.*;
import htsjdk.samtools.seekablestream.SeekableStream;
import htsjdk.samtools.util.CloseableIterator;
import org.apache.log4j.Logger;
import org.broad.igv.exceptions.DataLoadException;
import org.broad.igv.sam.EmptyAlignmentIterator;
import org.broad.igv.sam.PicardAlignment;
import org.broad.igv.sam.cram.IGVReferenceSource;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.FileUtils;
import org.broad.igv.util.HttpUtils;
import org.broad.igv.util.ResourceLocator;
import org.broad.igv.util.stream.IGVSeekableBufferedStream;
import org.broad.igv.util.stream.IGVSeekableStreamFactory;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* Created by IntelliJ IDEA.
* User: jrobinso
* Date: Sep 22, 2009
* Time: 2:21:04 PM
*/
public class BAMReader implements AlignmentReader<PicardAlignment> {
static Logger log = Logger.getLogger(BAMReader.class);
private final ResourceLocator locator;
SAMFileHeader header;
htsjdk.samtools.SamReader reader;
List<String> sequenceNames;
private boolean indexed = false; // False until proven otherwise
public BAMReader(ResourceLocator locator, boolean requireIndex) throws IOException {
this.locator = locator;
reader = getSamReader(locator, requireIndex);
header = reader.getFileHeader();
validateSequenceLengths(header);
}
private void validateSequenceLengths(SAMFileHeader header) {
SAMSequenceDictionary dict = header.getSequenceDictionary();
for (SAMSequenceRecord seq : dict.getSequences()) {
if (seq.getSequenceLength() > 536870911) {
throw new RuntimeException("Sequence lengths > 2^29-1 are not supported");
}
}
}
private SamReader getSamReader(ResourceLocator locator, boolean requireIndex) throws IOException {
boolean isLocal = locator.isLocal();
final SamReaderFactory factory = SamReaderFactory.makeDefault().
referenceSource(new IGVReferenceSource()).
validationStringency(ValidationStringency.SILENT);
SamInputResource resource;
if (isLocal) {
resource = SamInputResource.of(new File(locator.getPath()));
} else {
URL url = new URL(locator.getPath());
if (requireIndex) {
resource = SamInputResource.of(new IGVSeekableBufferedStream(IGVSeekableStreamFactory.getInstance().getStreamFor(url), 128000));
} else {
resource = SamInputResource.of(HttpUtils.getInstance().openConnectionStream(url));
}
}
if (requireIndex) {
String indexPath = getExplicitIndexPath(locator);
if (indexPath == null) {
indexPath = getIndexPath(locator.getPath());
}
indexed = true;
if (isLocal) {
File indexFile = new File(indexPath);
resource = resource.index(indexFile);
} else {
SeekableStream indexStream = IGVSeekableStreamFactory.getInstance().getStreamFor(new URL(indexPath));
resource = resource.index(indexStream);
}
}
return factory.open(resource);
}
public void close() throws IOException {
if (reader != null) {
reader.close();
}
}
public SAMFileHeader getFileHeader() {
if (header == null) {
header = reader.getFileHeader();
}
return header;
}
public boolean hasIndex() {
return indexed;
}
public Set<String> getPlatforms() {
return AlignmentReaderFactory.getPlatforms(getFileHeader());
}
public List<String> getSequenceNames() {
if (sequenceNames == null) {
SAMFileHeader header = getFileHeader();
if (header == null) {
return null;
}
sequenceNames = new ArrayList();
List<SAMSequenceRecord> records = header.getSequenceDictionary().getSequences();
if (records.size() > 0) {
for (SAMSequenceRecord rec : header.getSequenceDictionary().getSequences()) {
String chr = rec.getSequenceName();
sequenceNames.add(chr);
}
}
}
return sequenceNames;
}
public CloseableIterator<PicardAlignment> iterator() {
return new WrappedIterator(reader.iterator());
}
public CloseableIterator<PicardAlignment> query(String sequence, int start, int end, boolean contained) {
CloseableIterator<SAMRecord> iter = null;
try {
iter = reader.query(sequence, start + 1, end, contained);
} catch (IllegalArgumentException e) {
log.error("Error querying for sequence: " + sequence, e);
return new EmptyAlignmentIterator();
}
return new WrappedIterator(iter);
}
/**
* Fetch an explicitly set index path, either via the ResourceLocator or as a parameter in a URL
*
* @param locator
* @return the index path, or null if no index path is set
*/
private String getExplicitIndexPath(ResourceLocator locator) {
String p = locator.getPath().toLowerCase();
String idx = locator.getIndexPath();
if (idx == null && (p.startsWith("http://") || p.startsWith("https://"))) {
try {
URL url = new URL(locator.getPath());
String queryString = url.getQuery();
if (queryString != null) {
Map<String, String> parameters = HttpUtils.parseQueryString(queryString);
if (parameters.containsKey("index")) {
idx = parameters.get("index");
}
}
} catch (MalformedURLException e) {
log.error("Error parsing url: " + locator.getPath());
}
}
return idx;
}
/**
* Try to guess the index path.
*
* @param pathOrURL
* @return
* @throws IOException
*/
private String getIndexPath(String pathOrURL) throws IOException {
List<String> pathsTried = new ArrayList<String>();
String indexPath;
if (FileUtils.isRemote(pathOrURL)) {
// Try .bam.bai
indexPath = getIndexURL(pathOrURL, ".bai");
pathsTried.add(indexPath);
if (HttpUtils.getInstance().resourceAvailable(new URL(indexPath))) {
return indexPath;
}
// Try .bai
if (pathOrURL.endsWith(".bam")) {
indexPath = getIndexURL(pathOrURL.substring(0, pathOrURL.length() - 4), ".bai");
pathsTried.add(indexPath);
if (HttpUtils.getInstance().resourceAvailable(new URL(indexPath))) {
return indexPath;
}
}
// Try cram
if(pathOrURL.endsWith(".cram")) {
indexPath = getIndexURL(pathOrURL, ".crai");
if (FileUtils.resourceExists(indexPath)) {
pathsTried.add(indexPath);
return indexPath;
}
}
} else {
// Local file
indexPath = pathOrURL + ".bai";
if (FileUtils.resourceExists(indexPath)) {
return indexPath;
}
if (pathOrURL.endsWith(".cram")) {
indexPath = pathOrURL + ".crai";
if (FileUtils.resourceExists(indexPath)) {
return indexPath;
}
}
if (indexPath.contains(".bam.bai")) {
indexPath = indexPath.replaceFirst(".bam.bai", ".bai");
pathsTried.add(indexPath);
if (FileUtils.resourceExists(indexPath)) {
return indexPath;
}
} else {
indexPath = indexPath.replaceFirst(".bai", ".bam.bai");
pathsTried.add(indexPath);
if (FileUtils.resourceExists(indexPath)) {
return indexPath;
}
}
}
String defaultValue = pathOrURL + (pathOrURL.endsWith(".cram") ? ".crai" : ".bai");
indexPath = MessageUtils.showInputDialog(
"Index is required, but no index found. Please enter path to index file:",
defaultValue);
if (indexPath != null && FileUtils.resourceExists(indexPath)) {
return indexPath;
}
String msg = "Index file not found. Tried ";
for (String p : pathsTried) {
msg += "<br>" + p;
}
throw new DataLoadException(msg, indexPath);
}
private String getIndexURL(String urlString, String extension) {
String indexPath = null;
try {
URL url = new URL(urlString);
String queryString = url.getQuery();
if (queryString == null) {
indexPath = urlString + extension;
} else {
Map<String, String> parameters = HttpUtils.parseQueryString(queryString);
if (parameters.containsKey("file")) {
String bamFile = parameters.get("file");
String bamIndexFile = bamFile + extension;
String newQueryString = queryString.replace(bamFile, bamIndexFile);
indexPath = urlString.replace(queryString, newQueryString);
} else {
indexPath = urlString.replace(url.getPath(), url.getPath() + extension);
}
}
} catch (MalformedURLException e) {
log.error(e.getMessage(), e);
}
return indexPath;
}
}