/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.engine.source;
import com.google.dart.engine.context.AnalysisContext;
import com.google.dart.engine.context.AnalysisException;
import com.google.dart.engine.internal.context.PerformanceStatistics;
import com.google.dart.engine.internal.context.TimestampedData;
import com.google.dart.engine.utilities.general.TimeCounter;
import com.google.dart.engine.utilities.general.TimeCounter.TimeCounterHandle;
import com.google.dart.engine.utilities.instrumentation.Instrumentation;
import com.google.dart.engine.utilities.instrumentation.InstrumentationBuilder;
import com.google.dart.engine.utilities.io.FileUtilities;
import com.google.dart.engine.utilities.translation.DartBlockBody;
import com.google.dart.engine.utilities.translation.DartOmit;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
/**
* Instances of the class {@code FileBasedSource} implement a source that represents a file.
*
* @coverage dart.engine.source
*/
public class FileBasedSource implements Source {
/**
* The URI from which this source was originally derived.
*/
private final URI uri;
/**
* The file represented by this source.
*/
private final File file;
/**
* The cached encoding for this source.
*/
private String encoding;
/**
* The character set used to decode bytes into characters.
*/
@DartOmit
private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");
/**
* Initialize a newly created source object.
*
* @param file the file represented by this source
*/
public FileBasedSource(File file) {
this(file.toURI(), file);
}
/**
* Initialize a newly created source object.
*
* @param file the file represented by this source
* @param uri the URI from which this source was originally derived
*/
public FileBasedSource(URI uri, File file) {
this.uri = uri;
this.file = file;
}
@Override
public boolean equals(Object object) {
return object != null && object instanceof FileBasedSource
&& file.equals(((FileBasedSource) object).file);
}
@Override
public boolean exists() {
return file.isFile();
}
@Override
public TimestampedData<CharSequence> getContents() throws Exception {
TimeCounterHandle handle = PerformanceStatistics.io.start();
try {
return getContentsFromFile();
} finally {
reportIfSlowIO(handle.stop());
}
}
@Override
@DartOmit
public void getContentsToReceiver(ContentReceiver receiver) throws Exception {
TimeCounterHandle handle = PerformanceStatistics.io.start();
try {
getContentsFromFileToReceiver(receiver);
} finally {
reportIfSlowIO(handle.stop());
}
}
@Override
public String getEncoding() {
if (encoding == null) {
encoding = uri.toString();
}
return encoding;
}
/**
* Return the file represented by this source. This is an internal method that is only intended to
* be used by subclasses of {@link UriResolver} that are designed to work with file-based sources.
*
* @return the file represented by this source
*/
public File getFile() {
return file;
}
@Override
public String getFullName() {
return file.getAbsolutePath();
}
@Override
public long getModificationStamp() {
return file.lastModified();
}
@Override
public String getShortName() {
return file.getName();
}
@Override
public URI getUri() {
return uri;
}
@Override
public UriKind getUriKind() {
String scheme = uri.getScheme();
if (scheme.equals(PackageUriResolver.PACKAGE_SCHEME)) {
return UriKind.PACKAGE_URI;
} else if (scheme.equals(DartUriResolver.DART_SCHEME)) {
return UriKind.DART_URI;
} else if (scheme.equals(FileUriResolver.FILE_SCHEME)) {
return UriKind.FILE_URI;
}
return UriKind.FILE_URI;
}
@Override
public int hashCode() {
return file.hashCode();
}
@Override
public boolean isInSystemLibrary() {
return uri.getScheme().equals(DartUriResolver.DART_SCHEME);
}
@Override
public URI resolveRelativeUri(URI containedUri) throws AnalysisException {
try {
URI baseUri = uri;
boolean isOpaque = uri.isOpaque();
if (isOpaque) {
String scheme = uri.getScheme();
String part = uri.getRawSchemeSpecificPart();
if (scheme.equals(DartUriResolver.DART_SCHEME) && part.indexOf('/') < 0) {
part = part + "/" + part + ".dart";
}
baseUri = new URI(scheme + ":/" + part);
}
URI result = baseUri.resolve(containedUri).normalize();
if (isOpaque) {
result = new URI(result.getScheme() + ":" + result.getRawSchemeSpecificPart().substring(1));
}
return result;
} catch (Exception exception) {
throw new AnalysisException("Could not resolve URI (" + containedUri
+ ") relative to source (" + uri + ")", exception);
}
}
@Override
public String toString() {
if (file == null) {
return "<unknown source>";
}
return file.getAbsolutePath();
}
/**
* Get the contents and timestamp of the underlying file.
* <p>
* Clients should consider using the the method {@link AnalysisContext#getContents(Source)}
* because contexts can have local overrides of the content of a source that the source is not
* aware of.
*
* @return the contents of the source paired with the modification stamp of the source
* @throws Exception if the contents of this source could not be accessed
* @see #getContents()
*/
@DartBlockBody({"return new TimestampedData<String>(file.lastModified(), file.readAsStringSync());"})
protected TimestampedData<CharSequence> getContentsFromFile() throws Exception {
long modificationTime = file.lastModified();
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
FileChannel channel = null;
ByteBuffer byteBuffer = null;
try {
channel = randomAccessFile.getChannel();
long size = channel.size();
if (size > Integer.MAX_VALUE) {
throw new IllegalStateException("File is too long to be read");
}
int length = (int) size;
byte[] bytes = new byte[length];
byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.position(0);
byteBuffer.limit(length);
channel.read(byteBuffer);
} catch (ClosedByInterruptException exception) {
byteBuffer = null;
} finally {
try {
randomAccessFile.close();
} catch (IOException closeException) {
// Ignored
}
}
if (byteBuffer != null) {
byteBuffer.rewind();
skipOptionalBOM(byteBuffer);
return new TimestampedData<CharSequence>(modificationTime, UTF_8_CHARSET.decode(byteBuffer));
}
} catch (IOException exception) {
// Ignored so that we can try reading using non-native I/O
}
//
// Eclipse appears to be interrupting the thread sometimes. If we couldn't read the file using
// the native I/O support, try using the non-native support.
//
InputStreamReader reader = null;
String contents;
try {
reader = new InputStreamReader(getFileInputStreamWithoutBOM(), "UTF-8");
contents = FileUtilities.getContents(reader);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException closeException) {
// Ignored
}
}
}
return new TimestampedData<CharSequence>(modificationTime, contents);
}
/**
* Get the contents of underlying file and pass it to the given receiver.
*
* @param receiver the content receiver to which the content of this source will be passed
* @throws Exception if the contents of this source could not be accessed
* @see #getContentsToReceiver(ContentReceiver)
*/
@DartOmit
protected void getContentsFromFileToReceiver(ContentReceiver receiver) throws Exception {
long modificationTime = file.lastModified();
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
FileChannel channel = null;
ByteBuffer byteBuffer = null;
try {
channel = randomAccessFile.getChannel();
long size = channel.size();
if (size > Integer.MAX_VALUE) {
throw new IllegalStateException("File is too long to be read");
}
int length = (int) size;
byte[] bytes = new byte[length];
byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.position(0);
byteBuffer.limit(length);
channel.read(byteBuffer);
} catch (ClosedByInterruptException exception) {
byteBuffer = null;
} finally {
try {
randomAccessFile.close();
} catch (IOException closeException) {
// Ignored
}
}
if (byteBuffer != null) {
byteBuffer.rewind();
skipOptionalBOM(byteBuffer);
receiver.accept(UTF_8_CHARSET.decode(byteBuffer), modificationTime);
return;
}
} catch (IOException exception) {
// Ignored so that we can try reading using non-native I/O
}
//
// Eclipse appears to be interrupting the thread sometimes. If we couldn't read the file using
// the native I/O support, try using the non-native support.
//
InputStreamReader reader = null;
String contents;
try {
reader = new InputStreamReader(getFileInputStreamWithoutBOM(), "UTF-8");
contents = FileUtilities.getContents(reader);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException closeException) {
// Ignored
}
}
}
receiver.accept(contents, modificationTime);
}
/**
* Returns a {@link FileInputStream} for the {@link #file} with skipped optional leading UTF-8
* BOM.
*/
@DartOmit
private FileInputStream getFileInputStreamWithoutBOM() throws Exception {
FileInputStream in = new FileInputStream(file);
// check if there is an UTF-8 BOM
if (in.read() == (byte) 0xEF && in.read() == (byte) 0xBB && in.read() == (byte) 0xBF) {
return in;
}
// re-open stream
in.close();
return new FileInputStream(file);
}
/**
* Record the time the IO took if it was slow
*/
private void reportIfSlowIO(long nanos) {
//If slower than 10ms
if (nanos > 10 * TimeCounter.NANOS_PER_MILLI) {
InstrumentationBuilder builder = Instrumentation.builder("SlowIO");
try {
builder.data("fileName", getFullName());
builder.metric("IO-Time-Nanos", nanos);
} finally {
builder.log();
}
}
}
/**
* Skips an optional UTF-8 BOM.
*/
@DartOmit
private void skipOptionalBOM(ByteBuffer byteBuffer) {
if (byteBuffer.remaining() >= 3 && byteBuffer.get(0) == (byte) 0xEF
&& byteBuffer.get(1) == (byte) 0xBB && byteBuffer.get(2) == (byte) 0xBF) {
byteBuffer.position(3);
}
}
}