com.oracle.truffle.api.source.Source Maven / Gradle / Ivy
Show all versions of truffle-api Show documentation
/*
* Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.truffle.api.source;
import com.oracle.truffle.api.source.impl.SourceAccessor;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.spi.FileTypeDetector;
import java.util.Objects;
/**
* Representation of a source code unit and its contents. Source instances are created by using one
* of existing factory methods, each loading the file from a different source/medium.
*
* From a file on disk
*
* Each file is represented as a canonical object, indexed by the absolute, canonical path name of
* the file. File content is read lazily and may be optionally cached. Sample
* usage:
*
* {@link SourceSnippets#fromFile}
*
* The starting point is {@link Source#newBuilder(java.io.File)} method.
*
* Read from an URL
*
* One can read remote or in JAR resources using the {@link Source#newBuilder(java.net.URL)}
* factory:
*
* {@link SourceSnippets#fromURL}
*
* Each URL source is represented as a canonical object, indexed by the URL. Contents are read
* eagerly once the {@link Builder#build()} method is called.
*
* Source from a literal text
*
* An anonymous immutable code snippet can be created from a string via the
* {@link Source#newBuilder(java.lang.String) } factory method:
*
* {@link SourceSnippets#fromAString}
*
* the created {@link Source} doesn't have associated {@link #getMimeType() mime type}. One has to
* explicitly attach via {@link Builder#mimeType(java.lang.String)} method. The created
* {@link Source} doesn't have associated {@link #getName() name}, one has to attach it via
* {@link Builder#name(java.lang.String)} method.
*
* Reading from a stream
*
* If one has a {@link Reader} one can convert its content into a {@link Source} via
* {@link Source#newBuilder(java.io.Reader)} method:
*
* {@link SourceSnippets#fromReader}
*
* the content is read eagerly once the {@link Builder#build()} method is called. It
* doesn't have associated {@link #getMimeType() mime type} and {@link #getName()}. Both values have
* to be explicitly provided by {@link Builder#name(java.lang.String) } and
* {@link Builder#mimeType(java.lang.String)} methods otherwise {@link MissingMIMETypeException}
* and/or {@link MissingNameException} are thrown.
*
*
* Immutability of {@link Source}
*
*
* {@link Source} is an immutable object - once (lazily) loaded, it remains the same. The source
* object can be associated with various attributes like {@link #getName()} , {@link #getURI() ()},
* {@link #getMimeType()} and these are immutable as well. The system makes the best effort to
* derive values of these attributes from the location and/or content of the {@link Source} object.
* However, to give the user that creates the source control over these attributes, the API offers
* an easy way to alter values of these attributes by creating clones of the source via
* {@link Builder#mimeType(java.lang.String)}, {@link Builder#name(java.lang.String)},
* {@link Builder#uri(java.net.URI)} methods.
*
*
* While {@link Source} is immutable, the world around it is changing. The content of a file from
* which a {@link Source#newBuilder(java.io.File) source has been read} may change few seconds
* later. How can we balance the immutability with ability to see real state of the world? In this
* case, one can load of a new version of the {@link Source#newBuilder(java.io.File) source for the
* same file}. The newly loaded {@link Source} will be different than the previous one, however it
* will have the same attributes ({@link #getName()}, presumably also {@link #getMimeType()}, etc.).
* There isn't much to do about this - just keep in mind that there can be multiple different
* {@link Source} objects representing the same {@link #getURI() source origin}.
*
*
* @since 0.8 or earlier
*/
public abstract class Source {
// TODO (mlvdv) consider canonicalizing and reusing SourceSection instances
// TODO (mlvdv) connect SourceSections into a spatial tree for fast geometric lookup
private static final Source EMPTY = new SourceImpl(new LiteralSourceImpl("", ""));
private static final String NO_FASTPATH_SUBSOURCE_CREATION_MESSAGE = "do not create sub sources from compiled code";
private final Content content;
private final URI uri;
private final String name;
private String mimeType;
private final boolean internal;
private final boolean interactive;
private TextMap textMap;
/**
* Creates new {@link Source} builder for specified file
. Once the source is built
* the {@link Source#getName() name} will become {@link File#getName()} and the
* {@link Source#getCode()} will be loaded from the file, unless
* {@link Builder#content(java.lang.String) redefined} on the builder. Sample usage:
*
* {@link SourceSnippets#fromFile}
*
* The system tries to deduce appropriate {@link Source#getMimeType()} by consulting registered
* {@link FileTypeDetector file type detectors}.
*
* @param file the location of the file to load content from
* @return new instance of builder
* @since 0.15
*/
public static Builder newBuilder(File file) {
return EMPTY.new Builder<>(file);
}
/**
* Builds new {@link Source source} from a provided text. One needs to specify a
* {@link Builder#mimeType(java.lang.String)}, possibly a {@link Builder#name(java.lang.String)}
* and other attributes and then can {@link Builder#build()} a new instance of the source.
* Sample usage:
*
* {@link SourceSnippets#fromAString}
*
* @param text the text to be returned by {@link Source#getCode()}
* @return new builder to configure additional properties
* @since 0.15
*/
public static Builder newBuilder(String text) {
return EMPTY.new Builder<>(text);
}
/**
* Creates a {@linkplain Source Source instance} that represents the contents of a sub-range of
* an this
{@link Source}.
*
* @param baseCharIndex 0-based index of the first character of the sub-range
* @param length the number of characters in the sub-range
* @return a new instance representing a sub-range of another Source
* @throws IllegalArgumentException if the specified sub-range is not contained in the base
* @since 0.15
*/
public Source subSource(int baseCharIndex, int length) {
SourceAccessor.neverPartOfCompilation(NO_FASTPATH_SUBSOURCE_CREATION_MESSAGE);
final SubSourceImpl subSource = SubSourceImpl.create(this, baseCharIndex, length);
return new SourceImpl(subSource);
}
/**
* Creates a new source whose content will be read from the provided URL once it is
* {@link Builder#build() constructed}. Example:
*
* {@link SourceSnippets#fromURL}
*
* @param url the URL to read from and identify the source by
* @return new builder to configure and {@link Builder#build() construct} {@link Source} from
* @since 0.15
*/
public static Builder newBuilder(URL url) {
return EMPTY.new Builder<>(url);
}
/**
* Creates a new source whose content will be read once it is {@link Builder#build()
* constructed}. Multiple {@link Source} instances constructed by a single {@link Builder}
* instance share the content, read only once. When building source from reader, it is essential
* to {@link Builder#mimeType(java.lang.String) specify MIME type}. Example follows:
*
* {@link SourceSnippets#fromReader}
*
* @param reader reader to read the content from
* @return new builder to configure and {@link Builder#build() construct} {@link Source} from
* @since 0.15
*/
public static Builder newBuilder(Reader reader) {
return EMPTY.new Builder<>(reader);
}
static String read(File file) throws IOException {
return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
}
static String read(Reader reader) throws IOException {
final StringBuilder builder = new StringBuilder();
final char[] buffer = new char[1024];
try {
while (true) {
final int n = reader.read(buffer);
if (n == -1) {
break;
}
builder.append(buffer, 0, n);
}
} finally {
reader.close();
}
return builder.toString();
}
Source(Content content, String mimeType, URI uri, String name, boolean internal, boolean interactive) {
this.content = content;
this.mimeType = mimeType;
this.name = name;
this.internal = internal;
this.interactive = interactive;
this.uri = uri;
}
Content content() {
return content;
}
/**
* Returns the name of this resource holding a guest language program. An example would be the
* name of a guest language source code file. Name is supposed to be shorter than
* {@link #getPath()}.
*
* @return the name of the guest language program
* @since 0.8 or earlier
*/
public String getName() {
return name == null ? content().getName() : name;
}
/**
* The fully qualified name of the source. In case this source originates from a {@link File},
* then the default path is the normalized, {@link File#getCanonicalPath() canonical path}.
*
* @since 0.8 or earlier
*/
public String getPath() {
return content().getPath();
}
/**
* Check whether this source has been marked as internal, meaning that it has been
* provided by the infrastructure, language implementation, or system library. Internal
* sources are presumed to be irrelevant to guest language programmers, as well as possibly
* confusing and revealing of language implementation details.
*
* On the other hand, tools should be free to make internal sources visible in
* (possibly privileged) modes that are useful for language implementors.
*
* One can specify whether a source is internal when {@link Builder#internal() building it}.
*
* @return whether this source is marked as internal
* @since 0.15
*/
public boolean isInternal() {
return internal;
}
/**
* Check whether this source has been marked as interactive. Interactive sources are
* provided by an entity which is able to interactively read output and provide an input during
* the source execution; that can be a user I/O through an interactive shell for instance.
*
* Depending on {@link com.oracle.truffle.api.vm.PolyglotEngine.Language#isInteractive()
* language interactive} capability, when interactive sources are executed, the
* appropriate result could be passed directly to the polyglot engine
* {@link com.oracle.truffle.api.vm.PolyglotEngine.Builder#setOut(OutputStream) output stream}
* or {@link com.oracle.truffle.api.vm.PolyglotEngine.Builder#setErr(OutputStream) error stream}
* and polyglot engine
* {@link com.oracle.truffle.api.vm.PolyglotEngine.Builder#setIn(InputStream) input stream} can
* be used to read user input during the execution, to clarify the execution behavior by asking
* questions for instance. Non-interactive languages are expected to ignore this property.
*
* One can specify whether a source is interactive when {@link Builder#interactive() building
* it}.
*
* @return whether this source is marked as interactive
* @since 0.21
*/
public boolean isInteractive() {
return interactive;
}
/**
* The URL if the source is retrieved via URL.
*
* @return URL or null
* @since 0.8 or earlier
*/
public URL getURL() {
return content().getURL();
}
/**
* Get URI of the source. Every source has an associated {@link URI}, which can be used as a
* persistent identification of the source. For example one can
* {@link com.oracle.truffle.api.debug.DebuggerSession#install(com.oracle.truffle.api.debug.Breakpoint)
* register a breakpoint using a URI} to a source that isn't loaded yet and it will be activated
* when the source is
* {@link com.oracle.truffle.api.vm.PolyglotEngine#eval(com.oracle.truffle.api.source.Source)
* evaluated}. The {@link URI} returned by this method should be as unique as possible, yet it
* can happen that different {@link Source sources} return the same {@link #getURI} - for
* example when content of a {@link Source#newBuilder(java.io.File) file on a disk} changes and
* is re-loaded.
*
* @return a URI, it's never null
* @since 0.14
*/
public URI getURI() {
return uri == null ? content().getURI() : uri;
}
/**
* Access to the source contents.
*
* @since 0.8 or earlier
*/
public Reader getReader() {
try {
return content().getReader();
} catch (final IOException ex) {
return new Reader() {
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
throw ex;
}
@Override
public void close() throws IOException {
}
};
}
}
/**
* Access to the source contents. Causes the contents of this source to be loaded if they are
* loaded lazily.
*
* @since 0.8 or earlier
*/
public final InputStream getInputStream() {
return new ByteArrayInputStream(getCode().getBytes());
}
/**
* Gets the number of characters in the source. Causes the contents of this source to be loaded
* if they are loaded lazily.
*
* @since 0.8 or earlier
*/
public final int getLength() {
return getTextMap().length();
}
/**
* Returns the complete text of the code. Causes the contents of this source to be loaded if
* they are loaded lazily.
*
* @since 0.8 or earlier
*/
public String getCode() {
return content().getCode();
}
/**
* Returns a subsection of the code test. Causes the contents of this source to be loaded if
* they are loaded lazily.
*
* @since 0.8 or earlier
*/
public String getCode(int charIndex, int charLength) {
return getCode().substring(charIndex, charIndex + charLength);
}
/**
* Gets the text (not including a possible terminating newline) in a (1-based) numbered line.
* Causes the contents of this source to be loaded if they are loaded lazily.
*
* @since 0.8 or earlier
*/
public final String getCode(int lineNumber) {
final int offset = getTextMap().lineStartOffset(lineNumber);
final int length = getTextMap().lineLength(lineNumber);
return getCode().substring(offset, offset + length);
}
/**
* The number of text lines in the source, including empty lines; characters at the end of the
* source without a terminating newline count as a line. Causes the contents of this source to
* be loaded if they are loaded lazily.
*
* @since 0.8 or earlier
*/
public final int getLineCount() {
return getTextMap().lineCount();
}
/**
* Given a 0-based character offset, return the 1-based number of the line that includes the
* position. Causes the contents of this source to be loaded if they are loaded lazily.
*
* @throws IllegalArgumentException if the offset is outside the text contents
* @since 0.8 or earlier
*/
public final int getLineNumber(int offset) throws IllegalArgumentException {
return getTextMap().offsetToLine(offset);
}
/**
* Given a 0-based character offset, return the 1-based number of the column at the position.
* Causes the contents of this source to be loaded if they are loaded lazily.
*
* @throws IllegalArgumentException if the offset is outside the text contents
* @since 0.8 or earlier
*/
public final int getColumnNumber(int offset) throws IllegalArgumentException {
return getTextMap().offsetToCol(offset);
}
/**
* Given a 1-based line number, return the 0-based offset of the first character in the line.
*
* @throws IllegalArgumentException if there is no such line in the text
* @since 0.8 or earlier
*/
public final int getLineStartOffset(int lineNumber) throws IllegalArgumentException {
return getTextMap().lineStartOffset(lineNumber);
}
/**
* The number of characters (not counting a possible terminating newline) in a (1-based)
* numbered line. Causes the contents of this source to be loaded if they are loaded lazily.
*
* @throws IllegalArgumentException if there is no such line in the text
* @since 0.8 or earlier
*/
public final int getLineLength(int lineNumber) throws IllegalArgumentException {
return getTextMap().lineLength(lineNumber);
}
/**
* Returns an unavailable source section indicating that the source location is not available.
* Unavailable source sections have the same characteristics as empty source sections with
* character index 0
, but returns false
for
* {@link SourceSection#isAvailable()}.
*
* @see SourceSection#isAvailable()
* @since 0.18
*/
public final SourceSection createUnavailableSection() {
return new SourceSection(this, 0, -1);
}
/**
* Creates a representation of a line of text in the source identified only by line number, from
* which the character information will be computed. Please note that calling this method does
* cause the {@link Source#getCode() code} of this source to be loaded.
*
* @param lineNumber 1-based line number of the first character in the section
* @return newly created object representing the specified line
* @throws IllegalArgumentException if the given lineNumber does not exist the source
* @since 0.17
*/
public final SourceSection createSection(int lineNumber) {
if (lineNumber < 1) {
throw new IllegalArgumentException("lineNumber < 1");
}
final int charIndex = getTextMap().lineStartOffset(lineNumber);
final int length = getTextMap().lineLength(lineNumber);
SourceSection section = new SourceSection(this, charIndex, length);
assert assertValid(section);
return section;
}
/**
* Creates a representation of a contiguous region of text in the source. Please note that
* calling this method does only cause the {@link Source#getCode() code} of this source to be
* loaded if assertions enabled. The bounds of the source section are only verified if
* assertions (-ea) are enabled in the host system. An {@link IllegalArgumentException} is
* thrown if the given indices are out of bounds of the source bounds.
*
* @param charIndex 0-based position of the first character in the section
* @param length the number of characters in the section
* @return newly created object representing the specified region
* @throws IllegalArgumentException if charIndex < 0 or length < 0; in case assertions are
* enabled also if the given bounds are out of the source bounds.
* @since 0.17
*/
public final SourceSection createSection(int charIndex, int length) {
if (charIndex < 0) {
throw new IllegalArgumentException("charIndex < 0");
} else if (length < 0) {
throw new IllegalArgumentException("length < 0");
}
SourceSection section = new SourceSection(this, charIndex, length);
assert assertValid(section);
return section;
}
/**
* Creates a representation of a contiguous region of text in the source. Computes the
* {@code charIndex} value by building a text map of lines in the source. Please note that
* calling this method does cause the {@link Source#getCode() code} of this source to be loaded.
*
* @param startLine 1-based line number of the first character in the section
* @param startColumn 1-based column number of the first character in the section
* @param length the number of characters in the section
* @return newly created object representing the specified region
* @throws IllegalArgumentException if arguments are outside the text of the source bounds
* @see #createSection(int, int)
* @since 0.17
*/
public final SourceSection createSection(int startLine, int startColumn, int length) {
if (startLine <= 0) {
throw new IllegalArgumentException("startLine < 1");
} else if (startColumn <= 0) {
throw new IllegalArgumentException("startColumn < 1");
} else if (length < 0) {
throw new IllegalArgumentException("length < 0");
}
final int lineStartOffset = getTextMap().lineStartOffset(startLine);
if (startColumn > getTextMap().lineLength(startLine)) {
throw new IllegalArgumentException("column out of range");
}
final int charIndex = lineStartOffset + startColumn - 1;
if (charIndex + length > getCode().length()) {
throw new IllegalArgumentException("charIndex out of range");
}
SourceSection section = new SourceSection(this, charIndex, length);
assert assertValid(section);
return section;
}
private static boolean assertValid(SourceSection section) {
if (!section.isValid()) {
throw new IllegalArgumentException("Invalid source section bounds.");
}
return true;
}
/**
* Creates a representation of a line number in this source, suitable for use as a hash table
* key with equality defined to mean equivalent location.
*
* @param lineNumber a 1-based line number in this source
* @return a representation of a line in this source
* @since 0.8 or earlier
* @deprecated without replacement
*/
@SuppressWarnings("deprecation")
@Deprecated
public final LineLocation createLineLocation(int lineNumber) {
return new LineLocation(this, lineNumber);
}
/**
* An object suitable for using as a key into a hashtable that defines equivalence between
* different source types.
*/
Object getHashKey() {
return content().getHashKey();
}
final TextMap getTextMap() {
if (textMap == null) {
textMap = createTextMap();
}
return textMap;
}
final void clearTextMap() {
textMap = null;
}
TextMap createTextMap() {
final String code = getCode();
if (code == null) {
throw new RuntimeException("can't read file " + getName());
}
return TextMap.fromString(code);
}
/**
* MIME type that is associated with this source. By default file extensions known to the system
* are used to determine the MIME type (via registered {@link FileTypeDetector} classes), yet
* one can directly {@link Builder#mimeType(java.lang.String) provide a MIME type} to each
* source.
*
* @return MIME type of this source or null
, if unknown
* @since 0.8 or earlier
*/
public String getMimeType() {
if (mimeType == null) {
try {
mimeType = content().findMimeType();
} catch (IOException ex) {
// swallow and return null
}
}
return mimeType;
}
final boolean equalAttributes(Source other) {
return Objects.equals(getMimeType(), other.getMimeType()) &&
Objects.equals(getName(), other.getName()) &&
Objects.equals(getPath(), other.getPath());
}
@SuppressWarnings({"unchecked", "unused"})
static E raise(Class type, Exception ex) throws E {
throw (E) ex;
}
/**
* Allows one to specify additional attribute before {@link #build() creating} new
* {@link Source} instance. One can specify {@link #name(java.lang.String)},
* {@link #mimeType(java.lang.String)}, {@link #content(java.lang.String)} and/or whether a
* {@link Source} is {@link #internal() internal} or not.
*
* To load a source from disk one can use:
*
* {@link SourceSnippets#fromFile}
*
* To load source from a {@link URL} one can use:
*
* {@link SourceSnippets#fromURL}
*
* To create a source representing text in a string use:
*
* {@link SourceSnippets#fromAString}
*
* or read a source from a {@link Reader}:
*
* {@link SourceSnippets#fromReader}
*
*
* The system does all it can to guarantee that newly created {@link Source source} has a
* {@link Source#getMimeType() MIME type assigned}. In some situations the mime type can be
* guessed, in others it has to be explicitly specified via the
* {@link #mimeType(java.lang.String)} method.
*
* Once your builder is configured, call {@link #build()} to perform the loading and
* construction of new {@link Source}.
*
* @param the (checked) exception that one should expect when calling {@link #build()}
* method - usually an {@link IOException},
* {@link Source#newBuilder(java.lang.String) sometimes} none.
* @param either a {@link MissingMIMETypeException} to signal that one has to call
* {@link #mimeType(java.lang.String)} or a {@link RuntimeException} to signal
* everything seems to be OK
* @param either a {@link MissingNameException} to signal that one has to call
* {@link #name(java.lang.String)} or a {@link RuntimeException} to signal everything
* seems to be OK
* @since 0.15
*/
public final class Builder {
private final Object origin;
private URI uri;
private String name;
private String path;
private String mime;
private String content;
private boolean internal;
private boolean interactive;
private Builder(Object origin) {
this.origin = origin;
}
/**
* Gives a new name to the {@link #build() to-be-built} {@link Source}.
*
* @param newName name that replaces the previously given one, cannot be null
* @return instance of this
builder
* @since 0.15
*/
@SuppressWarnings("unchecked")
public Builder name(String newName) {
Objects.requireNonNull(newName);
this.name = newName;
return (Builder) this;
}
Builder path(String p) {
this.path = p;
return this;
}
/**
* Explicitly assignes a {@link Source#getMimeType() MIME type} to the {@link #build()
* to-be-built} {@link Source}. This method returns the builder parametrized with
* {@link Source} type parameter to signal to the compiler that it is safe to call
* {@link #build()} method and create an instance of a {@link Source}. Example:
*
* {@link SourceSnippets#fromAString}
*
* @param newMimeType the new mime type to be assigned
* @return instance of this
builder ready to {@link #build() create new source}
* @since 0.15
*/
@SuppressWarnings("unchecked")
public Builder mimeType(String newMimeType) {
Objects.requireNonNull(newMimeType);
this.mime = newMimeType;
return (Builder) this;
}
/**
* Marks the source as internal. Internal sources are those that aren't created by user, but
* rather inherently present by the language system. Calling this method influences result
* of create {@link Source#isInternal()}
*
* @return the instance of this builder
* @since 0.15
*/
public Builder internal() {
this.internal = true;
return this;
}
/**
* Marks the source as interactive. {@link com.oracle.truffle.api.vm.PolyglotEngine#eval
* Evaluation} of interactive sources by an
* {@link com.oracle.truffle.api.vm.PolyglotEngine.Language#isInteractive() interactive
* language} can use the {@link com.oracle.truffle.api.vm.PolyglotEngine} streams to print
* the result and read an input. However, non-interactive languages are expected to ignore
* the interactive property of sources and not use the polyglot engine streams. Any desired
* printing of the evaluated result provided by a non-interactive language needs to be
* handled by the caller. Calling of this method influences the result of
* {@link Source#isInteractive()}.
*
* @return the instance of this builder
* @since 0.21
*/
public Builder interactive() {
this.interactive = true;
return this;
}
/**
* Assigns new {@link URI} to the {@link #build() to-be-created} {@link Source}. Each source
* provides {@link Source#getURI()} as a persistent identification of its location. A
* default value for the method is deduced from the location or content, but one can change
* it by using this method
*
* @param ownUri the URL to use instead of default one, cannot be null
* @return the instance of this builder
* @since 0.15
*/
public Builder uri(URI ownUri) {
Objects.requireNonNull(ownUri);
this.uri = ownUri;
return this;
}
/**
* Specifies content of {@link #build() to-be-built} {@link Source}. Using this method one
* can ignore the real content of a file or URL and use already read one, or completely
* different one. Example:
*
* {@link SourceSnippets#fromURLWithOwnContent}
*
* @param code the code to be available via {@link Source#getCode()}
* @return instance of this builder - which's {@link #build()} method no longer throws an
* {@link IOException}
* @since 0.15
*/
@SuppressWarnings("unchecked")
public Builder content(String code) {
this.content = code;
return (Builder) this;
}
Builder content(byte[] arr, int offset, int length, Charset encoding) {
this.content = new String(arr, offset, length, encoding);
return this;
}
/**
* Uses configuration of this builder to create new {@link Source} object. The return value
* is parametrized to ensure your code doesn't compile until you specify a MIME type:
*
* - either via file related methods like {@link Source#newBuilder(java.io.File)} that can
* guess the MIME type
* - or directly via {@link #mimeType(java.lang.String)} method on this builder
*
* This method may throw an exception - especially when dealing with files (e.g.
* {@link Source#newBuilder(java.net.URL)}, {@link Source#newBuilder(java.io.File)} or
* {@link Source#newBuilder(java.io.Reader)} this method may throw {@link IOException} that
* one needs to deal with. In case of other building styles (like
* {@link Source#newBuilder(java.lang.String)} one doesn't need to capture any exception
* when calling this method.
*
* @return the source object
* @throws E1 exception if something went wrong while creating the source
* @throws E2 eliminate this exception by calling {@link #mimeType(java.lang.String) }
* @throws E3 eliminate this exception by calling {@link #name(java.lang.String) }
* @since 0.15
*/
public Source build() throws E1, E2, E3 {
Content holder;
try {
if (origin instanceof File) {
holder = buildFile(content == null);
} else if (origin instanceof Reader) {
holder = buildReader();
} else if (origin instanceof URL) {
holder = buildURL();
} else {
holder = buildString();
}
String type = this.mime == null ? holder.findMimeType() : this.mime;
if (type == null) {
throw raise(RuntimeException.class, new MissingMIMETypeException());
}
if (content != null) {
holder.code = content;
}
SourceImpl ret = new SourceImpl(holder, type, uri, name, internal, interactive);
if (ret.getName() == null) {
throw raise(RuntimeException.class, new MissingNameException());
}
return ret;
} catch (IOException ex) {
throw raise(RuntimeException.class, ex);
}
}
private Content buildFile(boolean read) throws IOException {
final File file = (File) origin;
File absoluteFile = file.getCanonicalFile();
FileSourceImpl fileSource = new FileSourceImpl(
read ? Source.read(file) : null,
absoluteFile,
name == null ? file.getName() : name,
path == null ? absoluteFile.getPath() : path);
return fileSource;
}
private Content buildReader() throws IOException {
final Reader r = (Reader) origin;
if (content == null) {
content = read(r);
}
r.close();
LiteralSourceImpl ret = new LiteralSourceImpl(
null, content);
return ret;
}
private Content buildURL() throws IOException {
final URL url = (URL) origin;
String computedName = url.getPath().substring(url.getPath().lastIndexOf('/') + 1);
URLSourceImpl ret = new URLSourceImpl(url, content, computedName);
return ret;
}
private Content buildString() {
final String r = (String) origin;
if (content == null) {
content = r;
}
LiteralSourceImpl ret = new LiteralSourceImpl(
null, content);
return ret;
}
}
}
// @formatter:off
// Checkstyle: stop
class SourceSnippets {
public static Source fromFile(File dir, String name) throws IOException {
// BEGIN: SourceSnippets#fromFile
File file = new File(dir, name);
assert name.endsWith(".java") : "Imagine 'c:\\sources\\Example.java' file";
Source source = Source.newBuilder(file).build();
assert file.getName().equals(source.getName());
assert file.getPath().equals(source.getPath());
assert file.toURI().equals(source.getURI());
assert "text/x-java".equals(source.getMimeType());
// END: SourceSnippets#fromFile
return source;
}
public static Source likeFileName(String fileName) throws IOException {
// BEGIN: SourceSnippets#likeFileName
File file = new File(fileName);
Source source = Source.newBuilder(file.getCanonicalFile()).
name(file.getPath()).
build();
// END: SourceSnippets#likeFileName
return source;
}
public static Source fromURL(Class> relativeClass) throws IOException, URISyntaxException {
// BEGIN: SourceSnippets#fromURL
URL resource = relativeClass.getResource("sample.js");
Source source = Source.newBuilder(resource)
.name("sample.js")
.build();
assert resource.toExternalForm().equals(source.getPath());
assert "sample.js".equals(source.getName());
assert "application/javascript".equals(source.getMimeType());
assert resource.toURI().equals(source.getURI());
// END: SourceSnippets#fromURL
return source;
}
public static Source fromURLWithOwnContent(Class> relativeClass) {
// BEGIN: SourceSnippets#fromURLWithOwnContent
URL resource = relativeClass.getResource("sample.js");
Source source = Source.newBuilder(resource)
.name("sample.js")
.content("{}")
.build();
assert resource.toExternalForm().equals(source.getPath());
assert "sample.js".equals(source.getName());
assert "application/javascript".equals(source.getMimeType());
assert resource.toExternalForm().equals(source.getURI().toString());
assert "{}".equals(source.getCode());
// END: SourceSnippets#fromURLWithOwnContent
return source;
}
public static Source fromReader(Class> relativeClass) throws IOException {
// BEGIN: SourceSnippets#fromReader
Reader stream = new InputStreamReader(
relativeClass.getResourceAsStream("sample.js")
);
Source source = Source.newBuilder(stream)
.name("sample.js")
.mimeType("application/javascript")
.build();
assert "sample.js".equals(source.getName());
assert "application/javascript".equals(source.getMimeType());
// END: SourceSnippets#fromReader
return source;
}
public static Source fromAString() {
// BEGIN: SourceSnippets#fromAString
Source source = Source.newBuilder("function() {\n"
+ " return 'Hi';\n"
+ "}\n")
.name("hi.js")
.mimeType("application/javascript")
.build();
assert "hi.js".equals(source.getName());
assert "application/javascript".equals(source.getMimeType());
// END: SourceSnippets#fromAString
return source;
}
public static boolean loaded = true;
}
// @formatter:on