/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.coverage.sql;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.Future;
import java.util.concurrent.CancellationException;
import java.io.IOException;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.ImageWriter;
import org.opengis.util.InternationalString;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.referencing.operation.MathTransform2D;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.coverage.AbstractCoverage;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.geotoolkit.coverage.io.GridCoverageWriter;
import org.geotoolkit.coverage.io.GridCoverageWriteParam;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.ImageCoverageWriter;
import org.geotoolkit.internal.sql.table.ConfigurationKey;
import org.geotoolkit.image.io.mosaic.Tile;
import org.geotoolkit.image.io.XImageIO;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.*;
import static org.geotoolkit.internal.InternalUtilities.firstNonNull;
/**
* A grid coverage writer for a layer. This class provides a way to write the data using only the
* {@link GridCoverageWriter} API, with {@linkplain #getOutput() output} of kind {@link Layer}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.21
*
* @see CoverageDatabase#createGridCoverageWriter(String)
* @see Layer#addCoverageReferences(Collection, CoverageDatabaseController)
*
* @since 3.20
* @module
*/
public class LayerCoverageWriter extends GridCoverageWriter {
/**
* The default image format to use if it can not be inferred from the existing series.
*/
private static final String DEFAULT_FORMAT = "PNG";
/**
* The default filename prefix to use if no name can be inferred from the coverage.
*/
private static final String DEFAULT_PREFIX = "IMG";
/**
* The coverage database which created this {@code LayerCoverageWriter}.
*/
protected final CoverageDatabase database;
/**
* The {@link ImageCoverageWriter} to use for writing the file.
* Will be created when first needed.
*/
private transient GridCoverageWriter writer;
/**
* A specialized implementation for the {@link LayerCoverageWriter#writer} field.
* If the user did not specified explicitely a format, then this encoder will use
* the format declared in the database rather than trying to guess from the file suffix.
*/
private static final class Encoder extends ImageCoverageWriter {
/**
* The provider for the image writers to be used by {@linkplain #writer}.
* Will be fetched in same time than {@link #writer}.
*/
private final ImageWriterSpi writerSpi;
/**
* Creates a new encoder which will use the given format for writing images.
*/
Encoder(final ImageWriterSpi writerSpi) {
this.writerSpi = writerSpi;
}
/**
* Creates an image writer that claim to be able to encode the given output.
* The image format will be the one declared in the Series table, unless the
* user specified explicitely an other format.
*/
@Override
protected ImageWriter createImageWriter(final String formatName,
final Object output, final RenderedImage image) throws IOException
{
if (formatName == null && writerSpi.canEncodeImage(image)) {
return writerSpi.createWriterInstance();
}
return super.createImageWriter(formatName, output, image);
}
}
/**
* The provider for the image readers that can read the images encoded by the writer.
* Will be fetched in same time than {@link #writer}.
*/
private transient ImageReaderSpi readerSpi;
/**
* The destination directory where to write the images.
* Will be created in same time than {@link #writer}.
*/
private transient File directory;
/**
* The suffix to append to filename. This string shall contains a leading dot.
*/
private transient String suffix;
/**
* Creates a new writer for the given database. The {@link #setOutput(Object)}
* method must be invoked before this writer can be used.
*
* @param database The database to used with this writer.
*/
protected LayerCoverageWriter(final CoverageDatabase database) {
this.database = database;
}
/**
* Creates a new writer for the given database and initializes its layer to the given value.
*
* @throws CoverageStoreException Declared for compilation raison, but should never happen.
*/
LayerCoverageWriter(final CoverageDatabase database, final Future<Layer> layer)
throws CoverageStoreException
{
this(database);
super.setOutput(layer);
}
/**
* Returns the object to use for formatting error messages.
*/
private Errors errors() {
return Errors.getResources(getLocale());
}
/**
* Ensures that the output is set.
*
* @throws CoverageStoreException Declared for compilation raison, but should never happen.
*/
private void ensureOutputSet() throws CoverageStoreException, IllegalStateException {
if (super.getOutput() == null) { // Use 'super' because we don't want to wait for Future.
throw new IllegalStateException(errors().getString(Errors.Keys.NoImageOutput));
}
}
/**
* Returns the current layer which is used as output, or {@code null} if none.
*/
@Override
public final Layer getOutput() throws CoverageStoreException {
Object output = super.getOutput();
if (output instanceof Future<?>) {
output = ((FutureQuery<?>) output).result();
super.setOutput(output);
}
return (Layer) output;
}
/**
* Sets a new layer as output. The given input can be either a {@link Layer} instance,
* or the name of a layer as a {@link CharSequence}.
*
* @param output The new output as a {@link Layer} instance or a {@link CharSequence},
* or {@code null} for removing any output previously set.
* @throws IllegalArgumentException If the given output is not of a legal type.
*/
@Override
public void setOutput(Object output) throws CoverageStoreException {
if (output != null) {
if (output instanceof CharSequence) {
output = database.getLayer(output.toString());
} else if (!(output instanceof Layer)) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalClass_2,
output.getClass(), Layer.class));
}
}
clearCache();
super.setOutput(output);
}
/**
* Returns the name of the given coverage, or {@code null} if none.
*/
private static String getName(final GridCoverage coverage) throws CoverageStoreException {
if (coverage instanceof AbstractCoverage) {
final InternationalString i18n = ((AbstractCoverage) coverage).getName();
if (i18n != null) {
String name = i18n.toString();
if (name != null && !(name = name.trim()).isEmpty()) {
return name;
}
}
}
return null;
}
/**
* Writes a single grid coverage. The default implementation delegates to
* {@link #write(Iterable, GridCoverageWriteParam)}.
*/
@Override
public void write(final GridCoverage coverage, final GridCoverageWriteParam param)
throws CoverageStoreException, CancellationException
{
ensureNonNull("coverage", coverage);
write(Collections.singleton(coverage), param);
}
/**
* Writes one or many grid coverages. This method is a "all of nothing" operation:
* if an exception occurred while writing an image, then this methods will rollback
* the database transaction and delete any image files that this method invocation
* may have created.
* <p>
* <b>Notes:</b>
* <ul>
* <li>If the coverage {@linkplain AbstractCoverage#getName() has a name}, the name will
* be used as filename. Otherwise a default name will be generated.</li>
* <li>If a series already exists or the layer, then the directory and image format of
* that series will be used. Otherwise this method will use default values.</li>
* </ul>
*
* @see Layer#addCoverageReferences(Collection, CoverageDatabaseController)
*/
@Override
public void write(final Iterable<? extends GridCoverage> coverages, final GridCoverageWriteParam param)
throws CoverageStoreException, CancellationException
{
ensureNonNull("coverages", coverages);
ensureOutputSet();
/*
* Infers the image format, file suffix and target directory.
* The current implementation takes the most frequently used
* ones by scanning the GridCoverages and Series table.
*/
final Layer layer = getOutput();
if (writer == null) {
directory = firstNonNull(layer.getImageDirectories());
if (directory == null) {
directory = new File(database.database.getProperty(ConfigurationKey.ROOT_DIRECTORY), layer.getName());
if (!directory.isDirectory() && !directory.mkdirs()) {
throw new CoverageStoreException(Errors.format(Errors.Keys.CantCreateDirectory_1, directory));
}
}
String formatName = (param != null) ? param.getFormatName() : null;
if (formatName == null) {
formatName = firstNonNull(layer.getImageFormats());
if (formatName == null) {
formatName = DEFAULT_FORMAT;
}
}
final ImageWriterSpi writerSpi;
writerSpi = XImageIO.getWriterSpiByFormatName(formatName);
readerSpi = XImageIO.getImageReaderSpi(writerSpi);
if (readerSpi == null) {
readerSpi = XImageIO.getReaderSpiByFormatName(formatName);
}
suffix = XImageIO.getFileSuffix(writerSpi);
if (suffix == null) {
suffix = XImageIO.getFileSuffix(readerSpi);
}
writer = new Encoder(writerSpi);
}
/*
* Build a list of image files and write the images immediately. After we have built
* the full list and written all images, insert the entries in the database. If any
* error occurs, we will rollback the database transaction and delete all images that
* we created.
*/
final List<Tile> files = new ArrayList<>();
try {
for (final GridCoverage coverage : coverages) {
final File file;
String filename = getName(coverage);
if (filename != null) {
if (suffix != null) {
filename += suffix;
}
file = new File(directory, filename);
if (file.exists()) { // Check must be before to add to the files list.
throw new CoverageStoreException(errors().getString(Errors.Keys.FileAlreadyExists_1, file));
}
} else try {
// Not really a temporary file, but this method will create a unique filename.
file = File.createTempFile(DEFAULT_PREFIX, suffix, directory);
} catch (IOException e) {
throw new CoverageStoreException(errors().getString(Errors.Keys.CantWriteFile_1, DEFAULT_PREFIX), e);
}
/*
* Extract the grid geometry information and store them as a "tile". Even if the
* image is not really a tile, this is a convenient way to carry those information.
* Write the image, then insert entries in the database only after every images have
* been written. In case of failure, the images will be deleted in the catch block.
*/
final GridGeometry2D gridGeometry = GridGeometry2D.castOrCopy(coverage.getGridGeometry());
final MathTransform2D gridToCRS = gridGeometry.getGridToCRS2D();
if (!(gridToCRS instanceof AffineTransform)) {
throw new CoverageStoreException(errors().getString(Errors.Keys.NonAffineTransform));
}
files.add(new NewGridCoverage(readerSpi, file, gridGeometry, (AffineTransform) gridToCRS));
writer.setOutput(file);
writer.write(coverage, param);
}
writer.reset();
layer.addCoverageReferences(files, null);
} catch (Throwable e) {
/*
* Operation failed - delete all image files created by this method call.
*/
try {
writer.reset(); // Ensures that the file is closed.
} catch (Throwable s) {
e.addSuppressed(s);
}
for (final Tile tile : files) {
final File file = (File) tile.getInput();
if (!file.delete()) {
Logging.getLogger("org.geotoolkit.coverage.sql").warning(errors().getString(Errors.Keys.CantDeleteFile_1, file));
}
}
throw e;
}
}
/**
* Clears the cached object. This method needs to be invoked when the output changed,
* in order to force the calculation of new objects for the new output.
*/
private void clearCache() {
writer = null;
readerSpi = null;
directory = null;
suffix = null;
}
/**
* {@inheritDoc}
*/
@Override
public void reset() throws CoverageStoreException {
clearCache();
super.reset();
}
/**
* {@inheritDoc}
*/
@Override
public void dispose() throws CoverageStoreException {
clearCache();
super.dispose();
}
}