blob: 3a94c7bb5e0d25bbf953083c993f0303b566e5f3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.apache.freemarker.generator.base.datasource;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.freemarker.generator.base.activation.ByteArrayDataSource;
import org.apache.freemarker.generator.base.activation.StringDataSource;
import org.apache.freemarker.generator.base.mime.MimetypeParser;
import org.apache.freemarker.generator.base.util.CloseableReaper;
import javax.activation.FileDataSource;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.io.IOUtils.lineIterator;
import static org.apache.freemarker.generator.base.FreeMarkerConstants.DATASOURCE_UNKNOWN_LENGTH;
import static org.apache.freemarker.generator.base.util.StringUtils.emptyToNull;
import static org.apache.freemarker.generator.base.util.StringUtils.isNotEmpty;
/**
* Data source which encapsulates data to be used for rendering
* a template. When accessing content it is loaded on demand on not
* kept in memory to allow processing of large volumes of data.
* <br>
* There is also special support of <code>UrlDataSource</code> since
* the content type &amp; charset might be determined using a network
* call.
*/
public class DataSource implements Closeable {
/** Human-readable name of the data source */
private final String name;
/** Optional group of data source */
private final String group;
/** The URI for loading the content of the data source */
private final URI uri;
/** The underlying "javax.activation.DataSource" */
private final javax.activation.DataSource dataSource;
/** Optional user-supplied content type */
private final String contentType;
/** Optional charset for directly accessing text-based content */
private final Charset charset;
/** Collect all closables handed out to the caller to be closed when the data source is closed itself */
private final CloseableReaper closables;
public DataSource(
String name,
String group,
URI uri,
javax.activation.DataSource dataSource,
String contentType,
Charset charset) {
this.name = requireNonNull(name);
this.group = emptyToNull(group);
this.uri = requireNonNull(uri);
this.dataSource = requireNonNull(dataSource);
this.contentType = contentType;
this.charset = charset;
this.closables = new CloseableReaper();
}
public String getName() {
return name;
}
public String getGroup() {
return group;
}
public String getBaseName() {
return FilenameUtils.getBaseName(name);
}
public String getExtension() {
return FilenameUtils.getExtension(name);
}
public Charset getCharset() {
return charset != null ? charset : MimetypeParser.getCharset(contentType(), UTF_8);
}
/**
* Get the content type without additional parameters, e.g. "charset".
*
* @return content type
*/
public String getContentType() {
return MimetypeParser.getMimetype(contentType());
}
public URI getUri() {
return uri;
}
/**
* Try to get the length lazily, efficient and without consuming the input stream.
*
* @return Length of data source or UNKNOWN_LENGTH
*/
public long getLength() {
if (dataSource instanceof FileDataSource) {
return ((FileDataSource) dataSource).getFile().length();
} else if (dataSource instanceof StringDataSource) {
return ((StringDataSource) dataSource).length();
} else if (dataSource instanceof ByteArrayDataSource) {
return ((ByteArrayDataSource) dataSource).length();
} else {
return DATASOURCE_UNKNOWN_LENGTH;
}
}
/**
* Get an input stream which is closed together with this data source.
*
* @return InputStream
*/
public InputStream getInputStream() {
return closables.add(getUnsafeInputStream());
}
/**
* Get an input stream which needs to be closed by the caller.
*
* @return InputStream
*/
public InputStream getUnsafeInputStream() {
try {
return dataSource.getInputStream();
} catch (IOException e) {
throw new RuntimeException("Failed to get input stream: " + toString(), e);
}
}
public String getText() {
return getText(getCharset().name());
}
public String getText(String charsetName) {
final StringWriter writer = new StringWriter();
try (InputStream is = getUnsafeInputStream()) {
IOUtils.copy(is, writer, Charset.forName(charsetName));
return writer.toString();
} catch (IOException e) {
throw new RuntimeException("Failed to get text: " + toString(), e);
}
}
/**
* Gets the contents of an <code>InputStream</code> as a list of Strings,
* one entry per line, using the specified character encoding.
*
* @return the list of Strings, never null
*/
public List<String> getLines() {
return getLines(getCharset().name());
}
/**
* Gets the contents of an <code>InputStream</code> as a list of Strings,
* one entry per line, using the specified character encoding.
*
* @param charsetName The name of the requested charset
* @return the list of Strings, never null
*/
public List<String> getLines(String charsetName) {
try (InputStream inputStream = getUnsafeInputStream()) {
return IOUtils.readLines(inputStream, charsetName);
} catch (IOException e) {
throw new RuntimeException("Failed to get lines: " + toString(), e);
}
}
/**
* Returns an Iterator for the lines in an <code>InputStream</code>, using
* the default character encoding specified. The caller is responsible to close
* the line iterator.
*
* @return line iterator
*/
public LineIterator getLineIterator() {
return getLineIterator(getCharset().name());
}
/**
* Returns an Iterator for the lines in an <code>InputStream</code>, using
* the character encoding specified.
*
* @param charsetName The name of the requested charset
* @return line iterator
*/
public LineIterator getLineIterator(String charsetName) {
try {
return closables.add(lineIterator(getUnsafeInputStream(), Charset.forName(charsetName)));
} catch (IOException e) {
throw new RuntimeException("Failed to create line iterator: " + toString(), e);
}
}
public byte[] getBytes() {
try (InputStream inputStream = getUnsafeInputStream()) {
return IOUtils.toByteArray(inputStream);
} catch (IOException e) {
throw new RuntimeException("Failed to get bytes: " + toString(), e);
}
}
/**
* Some tools create a {@link java.io.Closeable} which can bound to the
* lifecycle of the data source. When the data source is closed all the
* associated {@link java.io.Closeable} are closed as well.
*
* @param closeable Closable
* @param <T> Type of closable
* @return Closable
*/
public <T extends Closeable> T addClosable(T closeable) {
return closables.add(closeable);
}
@Override
public void close() {
closables.close();
}
@Override
public String toString() {
return "DataSource{" +
"name='" + name + '\'' +
", group='" + group + '\'' +
", uri=" + uri +
'}';
}
private String contentType() {
return isNotEmpty(contentType) ? contentType : dataSource.getContentType();
}
}