Package org.apache.sis.storage

Source Code of org.apache.sis.storage.StorageConnector

/*
* 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.sis.storage;

import java.util.Map;
import java.util.Queue;
import java.util.Iterator;
import java.util.Collections;
import java.util.LinkedList;
import java.util.IdentityHashMap;
import java.util.ConcurrentModificationException;
import java.io.Reader;
import java.io.DataInput;
import java.io.InputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.channels.ReadableByteChannel;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import java.sql.Connection;
import javax.sql.DataSource;
import org.apache.sis.util.Debug;
import org.apache.sis.util.Classes;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.storage.IOUtilities;
import org.apache.sis.internal.storage.ChannelDataInput;
import org.apache.sis.internal.storage.ChannelImageInputStream;
import org.apache.sis.setup.OptionKey;

// Related to JDK7
import org.apache.sis.internal.jdk7.JDK7;


/**
* Information for creating a connection to a {@link DataStore} in read and/or write mode.
* {@code StorageConnector} wraps an input {@link Object}, which can be any of the following types:
*
* <ul>
*   <li>A {@link java.io.File} for a file or a directory.</li>
*   <li>A {@link java.net.URI} or a {@link java.net.URL} to a distant resource.</li>
*   <li>A {@link CharSequence} interpreted as a filename or a URL.</li>
*   <li>A {@link java.nio.channels.Channel}, {@link DataInput}, {@link InputStream} or {@link Reader}.</li>
*   <li>A {@link DataSource} or a {@link Connection} to a JDBC database.</li>
*   <li>Any other {@code DataStore}-specific object, for example {@link ucar.nc2.NetcdfFile}.</li>
* </ul>
*
* The {@link #getStorageAs(Class)} method provides the storage as an object of the given type, opening
* the input stream if necessary. This class tries to open the stream only once - subsequent invocation
* of {@code getStorageAs(…)} may return the same input stream.
*
* <p>This class is used only for discovery of a {@code DataStore} implementation capable to handle the input.
* Once a suitable {@code DataStore} has been found, the {@code StorageConnector} instance is typically
* discarded since each data store implementation will use their own input/output objects.</p>
*
* <p>Instances of this class are serializable if the {@code storage} object given at construction time
* is serializable.</p>
*
* @author  Martin Desruisseaux (Geomatys)
* @since   0.3
* @version 0.3
* @module
*/
public class StorageConnector implements Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = 2524083964906593093L;

    /**
     * The default size of the {@link ByteBuffer} to be created.
     * Users can override this value by providing a value for {@link OptionKey#BYTE_BUFFER}.
     */
    static final int DEFAULT_BUFFER_SIZE = 4096;

    /**
     * The minimal size of the {@link ByteBuffer} to be created. This size is used only
     * for temporary buffers that are unlikely to be used for the actual reading process.
     */
    static final int MINIMAL_BUFFER_SIZE = 256;

    /**
     * The input/output object given at construction time.
     *
     * @see #getStorage()
     */
    private final Object storage;

    /**
     * A name for the input/output object, or {@code null} if none.
     * This field is initialized only when first needed.
     */
    private transient String name;

    /**
     * The filename extension, or {@code null} if none.
     * This field is initialized only when first needed.
     */
    private transient String extension;

    /**
     * Views of {@link #storage} as some of the following supported types:
     *
     * <ul>
     *   <li>{@link ByteBuffer}:
     *       A read-only view of the buffer over the first bytes of the stream.</li>
     *
     *   <li>{@link DataInput}:
     *       The input as a data input stream. Unless the {@link #storage} is already an instance of {@link DataInput},
     *       this entry will be given an instance of {@link ChannelImageInputStream} if possible rather than an arbitrary
     *       stream. In particular, we invoke the {@link ImageIO#createImageInputStream(Object)} factory method only in
     *       last resort because some SIS data stores will want to access the channel and buffer directly.</li>
     *
     *   <li>{@link ImageInputStream}:
     *       Same as {@code DataInput} if it can be casted, or {@code null} otherwise.</li>
     *
     *   <li>{@link InputStream}:
     *       If not explicitely provided, this is a wrapper around the above {@link ImageInputStream}.</li>
     *
     *   <li>{@link Reader}:
     *       If not explicitely provided, this is a wrapper around the above {@link InputStream}.</li>
     *
     *   <li>{@link Connection}:
     *       The storage object as a JDBC connection.</li>
     * </ul>
     *
     * A non-existent entry means that the value has not yet been computed. A {@link Void#TYPE} value means the value
     * has been computed and we have determined that {@link #getStorageAs(Class)} shall returns {@code null} for that
     * type.
     *
     * @see #getStorageAs(Class)
     */
    private transient Map<Class<?>, Object> views;

    /**
     * Objects which will need to be closed by the {@link #closeAllExcept(Object)} method.
     * For each (<var>key</var>, <var>value</var>) entry, if the object to close (the key)
     * is a wrapper around an other object (e.g. an {@link InputStreamReader} wrapping an
     * {@link InputStream}), then the value is the other object.
     *
     * @see #addViewToClose(Object, Object)
     * @see #closeAllExcept(Object)
     */
    private transient Map<Object, Object> viewsToClose;

    /**
     * The options, created only when first needed.
     *
     * @see #getOption(OptionKey)
     * @see #setOption(OptionKey, Object)
     */
    private transient Map<OptionKey<?>, Object> options;

    /**
     * Creates a new data store connection wrapping the given input/output object.
     * The object can be of any type, but the class javadoc lists the most typical ones.
     *
     * @param storage The input/output object as a URL, file, image input stream, <i>etc.</i>.
     */
    public StorageConnector(final Object storage) {
        ArgumentChecks.ensureNonNull("storage", storage);
        this.storage = storage;
    }

    /**
     * Returns the option value for the given key, or {@code null} if none.
     *
     * @param  <T> The type of option value.
     * @param  key The option for which to get the value.
     * @return The current value for the given option, or {@code null} if none.
     */
    public <T> T getOption(final OptionKey<T> key) {
        ArgumentChecks.ensureNonNull("key", key);
        return key.getValueFrom(options);
    }

    /**
     * Sets the option value for the given key. The default implementation recognizes the following options:
     *
     * <ul>
     *   <li>{@link OptionKey#ENCODING}     for decoding characters in an input stream, if needed.</li>
     *   <li>{@link OptionKey#URL_ENCODING} for converting URL to URI or filename, if needed.</li>
     *   <li>{@link OptionKey#OPEN_OPTIONS} for specifying whether the data store shall be read only or read/write.</li>
     *   <li>{@link OptionKey#BYTE_BUFFER}  for allowing users to control the byte buffer to be created.</li>
     * </ul>
     *
     * @param <T>   The type of option value.
     * @param key   The option for which to set the value.
     * @param value The new value for the given option, or {@code null} for removing the value.
     */
    public <T> void setOption(final OptionKey<T> key, final T value) {
        ArgumentChecks.ensureNonNull("key", key);
        options = key.setValueInto(options, value);
    }

    /**
     * Returns the input/output object given at construction time.
     * The object can be of any type, but the class javadoc lists the most typical ones.
     *
     * @return The input/output object as a URL, file, image input stream, <i>etc.</i>.
     *
     * @see #getStorageAs(Class)
     */
    public Object getStorage() {
        return storage;
    }

    /**
     * Returns a short name of the input/output object. The default implementation performs
     * the following choices based on the type of the {@linkplain #getStorage() storage} object:
     *
     * <ul>
     *   <li>For {@link java.io.File}, {@link java.net.URI} or {@link java.net.URL}
     *       instances, this method uses dedicated API.</li>
     *   <li>For {@link CharSequence} instances, this method gets a string representation of the storage object
     *       and returns the part after the last {@code '/'} character or platform-dependent name separator.</li>
     *   <li>For instances of unknown type, this method builds a string representation using the class name.
     *       Note that the string representation of unknown types may change in any future SIS version.</li>
     * </ul>
     *
     * @return A short name of the storage object.
     */
    public String getStorageName() {
        if (name == null) {
            name = IOUtilities.filename(storage);
            if (name == null) {
                name = Classes.getShortClassName(storage);
            }
        }
        return name;
    }

    /**
     * Returns the filename extension of the input/output object. The default implementation performs
     * the following choices based on the type of the {@linkplain #getStorage() storage} object:
     *
     * <ul>
     *   <li>For {@link java.io.File}, {@link java.net.URI}, {@link java.net.URL} or
     *       {@link CharSequence} instances, this method returns the string after the last {@code '.'} character
     *       in the filename, provided that the {@code '.'} is not the first filename character. This may be an
     *       empty string if the filename has no extension, but never {@code null}.</li>
     *   <li>For instances of unknown type, this method returns {@code null}.</li>
     * </ul>
     *
     * @return The filename extension, or an empty string if none, or {@code null} if the storage
     *         is an object of unknown type.
     */
    public String getFileExtension() {
        if (extension == null) {
            extension = IOUtilities.extension(storage);
        }
        return extension;
    }

    /**
     * Returns the storage as a view of the given type if possible, or {@code null} otherwise.
     * The default implementation accepts the following types:
     *
     * <ul>
     *   <li>{@link String}:
     *     <ul>
     *       <li>If the {@linkplain #getStorage() storage} object is an instance of the
     *           {@link java.io.File}, {@link java.net.URL}, {@link java.net.URI} or {@link CharSequence} types,
     *           returns the string representation of their path.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     *   <li>{@link ByteBuffer}:
     *     <ul>
     *       <li>If the {@linkplain #getStorage() storage} object can be obtained as described in bullet 2 of the
     *           {@code DataInput} section below, then this method returns the associated byte buffer.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     *   <li>{@link DataInput}:
     *     <ul>
     *       <li>If the {@linkplain #getStorage() storage} object is already an instance of {@code DataInput}
     *           (including the {@link ImageInputStream} and {@link javax.imageio.stream.ImageOutputStream} types),
     *           then it is returned unchanged.</li>
     *
     *       <li>Otherwise if the input is an instance of {@link java.io.File},
     *           {@link java.net.URI}, {@link java.net.URL}, {@link CharSequence}, {@link InputStream} or
     *           {@link java.nio.channels.ReadableByteChannel}, then an {@link ImageInputStream} backed by a
     *           {@link ByteBuffer} is created when first needed and returned.</li>
     *
     *       <li>Otherwise if {@link ImageIO#createImageInputStream(Object)} returns a non-null value,
     *           then this value is cached and returned.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     *   <li>{@link ImageInputStream}:
     *     <ul>
     *       <li>If the above {@code DataInput} can be created and casted to {@code ImageInputStream}, returns it.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     *   <li>{@link InputStream}:
     *     <ul>
     *       <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link InputStream},
     *           then it is returned unchanged.</li>
     *
     *       <li>Otherwise if the above {@code ImageInputStream} can be created,
     *           returns a wrapper around that stream.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     *   <li>{@link Reader}:
     *     <ul>
     *       <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link Reader},
     *           then it is returned unchanged.</li>
     *
     *       <li>Otherwise if the above {@code InputStream} can be created, returns an {@link InputStreamReader}
     *           using the encoding specified by {@link OptionKey#ENCODING} if any, or using the system default
     *           encoding otherwise.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     *   <li>{@link Connection}:
     *     <ul>
     *       <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link Connection},
     *           then it is returned unchanged.</li>
     *
     *       <li>Otherwise if the storage is an instance of {@link DataSource}, then a connection is obtained
     *           when first needed and returned.</li>
     *
     *       <li>Otherwise this method returns {@code null}.</li>
     *     </ul>
     *   </li>
     * </ul>
     *
     * Multiple invocations of this method on the same {@code StorageConnector} instance will try
     * to return the same instance on a <cite>best effort</cite> basis. Consequently, implementations of
     * {@link DataStoreProvider#probeContent(StorageConnector)} methods shall not close the stream or
     * database connection returned by this method. In addition, those {@code probeContent(StorageConnector)}
     * methods are responsible for restoring the stream or byte buffer to its original position on return.
     *
     * @param  <T>  The compile-time type of the {@code type} argument.
     * @param  type The desired type as one of {@code ByteBuffer}, {@code DataInput}, {@code Connection}
     *         class or other type supported by {@code StorageConnector} subclasses.
     * @return The storage as a view of the given type, or {@code null} if no view can be created for the given type.
     * @throws IllegalArgumentException If the given {@code type} argument is not a known type.
     * @throws DataStoreException If an error occurred while opening a stream or database connection.
     *
     * @see #getStorage()
     * @see #closeAllExcept(Object)
     */
    public <T> T getStorageAs(final Class<T> type) throws IllegalArgumentException, DataStoreException {
        ArgumentChecks.ensureNonNull("type", type);
        if (views != null) {
            final Object view = views.get(type);
            if (view != null) {
                return (view != Void.TYPE) ? type.cast(view) : null;
            }
        } else {
            views = new IdentityHashMap<Class<?>, Object>();
        }
        /*
         * Special case for DataInput and ByteBuffer, because those values are created together.
         * In addition, ImageInputStream creation assigns a value to the 'streamOrigin' field.
         * The ChannelDataInput case is an undocumented (SIS internal) type for avoiding the
         * potential call to ImageIO.createImageInputStream(…) when we do not need it.
         */
        boolean done = false;
        try {
            if (type == ByteBuffer.class) {
                createByteBuffer();
                done = true;
            } else if (type == DataInput.class) {
                createDataInput();
                done = true;
            } else if (type == ChannelDataInput.class) { // Undocumented case (SIS internal)
                createChannelDataInput(false);
                done = true;
            }
        } catch (IOException e) {
            throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, getStorageName()), e);
        }
        if (done) {
            // Want to exit this method even if the value is null.
            return getView(type);
        }
        /*
         * All other cases.
         */
        final Object value;
        try {
            value = createView(type);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, getStorageName()), e);
        }
        final T view = type.cast(value);
        addView(type, view);
        return view;
    }

    /**
     * Creates a view for the input as a {@link ChannelDataInput} if possible.
     * If the view can not be created, remember that fact in order to avoid new attempts.
     *
     * @param  asImageInputStream If the {@code ChannelDataInput} needs to be {@link ChannelImageInputStream} subclass.
     * @throws IOException If an error occurred while opening a channel for the input.
     */
    private void createChannelDataInput(final boolean asImageInputStream) throws IOException {
        final ReadableByteChannel channel = IOUtilities.open(storage,
                getOption(OptionKey.URL_ENCODING), getOption(OptionKey.OPEN_OPTIONS));
        ChannelDataInput asDataInput = null;
        if (channel != null) {
            addViewToClose(channel, storage);
            ByteBuffer buffer = getOption(OptionKey.BYTE_BUFFER);
            if (buffer == null) {
                buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
                // TODO: we do not create direct buffer yet, but this is something
                // we may want to consider in a future SIS version.
            }
            final String name = getStorageName();
            if (asImageInputStream) {
                asDataInput = new ChannelImageInputStream(name, channel, buffer, false);
            } else {
                asDataInput = new ChannelDataInput(name, channel, buffer, false);
            }
            addViewToClose(asDataInput, channel);
        }
        addView(ChannelDataInput.class, asDataInput);
    }

    /**
     * Creates a view for the input as a {@link DataInput} if possible. This method performs the choice
     * documented in the {@link #getStorageAs(Class)} method for the {@code DataInput} case. Opening the
     * data input may imply creating a {@link ByteBuffer}, in which case the buffer will be stored under
     * the {@code ByteBuffer.class} key together with the {@code DataInput.class} case.
     *
     * @throws IOException If an error occurred while opening a stream for the input.
     */
    private void createDataInput() throws IOException {
        final DataInput asDataInput;
        if (storage instanceof DataInput) {
            asDataInput = (DataInput) storage;
        } else {
            /*
             * Creates a ChannelImageInputStream instance. We really need that specific type because some
             * SIS data stores will want to access directly the channel and the buffer. We will fallback
             * on the ImageIO.createImageInputStream(Object) method only in last resort.
             */
            if (!views.containsKey(ChannelDataInput.class)) {
                createChannelDataInput(true);
            }
            final ChannelDataInput c = getView(ChannelDataInput.class);
            if (c == null) {
                asDataInput = ImageIO.createImageInputStream(storage);
                addViewToClose(asDataInput, storage);
            } else if (c instanceof DataInput) {
                asDataInput = (DataInput) c;
                // No call to 'addViewToClose' because the instance already exists.
            } else {
                asDataInput = new ChannelImageInputStream(c);
                if (views.put(ChannelDataInput.class, asDataInput) != c) { // Replace the previous instance.
                    throw new ConcurrentModificationException();
                }
                addViewToClose(asDataInput, c.channel);
            }
        }
        addView(DataInput.class, asDataInput);
    }

    /**
     * Creates a {@link ByteBuffer} from the {@link ChannelDataInput} if possible, or from the
     * {@link ImageInputStream} otherwise. The buffer will be initialized with an arbitrary amount
     * of bytes read from the input. This amount is not sufficient, it can be increased by a call
     * to {@link #prefetch()}.
     *
     * @throws IOException If an error occurred while opening a stream for the input.
     */
    private void createByteBuffer() throws IOException, DataStoreException {
        /*
         * First, try to create the ChannelDataInput if it does not already exists.
         * If successful, this will create a ByteBuffer companion as a side effect.
         */
        if (!views.containsKey(ChannelDataInput.class)) {
            createChannelDataInput(false);
        }
        ByteBuffer asByteBuffer = null;
        final ChannelDataInput c = getView(ChannelDataInput.class);
        if (c != null) {
            asByteBuffer = c.buffer.asReadOnlyBuffer();
        } else {
            /*
             * If no ChannelDataInput has been create by the above code, get the input as an ImageInputStream and
             * read an arbitrary amount of bytes. Read only a small amount of bytes because, at the contrary of the
             * buffer created in createChannelDataInput(boolean), the buffer created here is unlikely to be used for
             * the reading process after the recognition of the file format.
             */
            final ImageInputStream in = getStorageAs(ImageInputStream.class);
            if (in != null) {
                in.mark();
                final byte[] buffer = new byte[MINIMAL_BUFFER_SIZE];
                final int n = in.read(buffer);
                in.reset();
                if (n >= 1) {
                    asByteBuffer = ByteBuffer.wrap(buffer).order(in.getByteOrder());
                    asByteBuffer.limit(n);
                    // Can't invoke asReadOnly() because 'prefetch()' need to be able to write in it.
                }
            }
        }
        addView(ByteBuffer.class, asByteBuffer);
    }

    /**
     * Transfers more bytes from the {@link DataInput} to the {@link ByteBuffer}, if possible.
     * This method returns {@code true} on success, or {@code false} if input is not a readable
     * channel or stream, we have reached the end of stream, or the buffer is full.
     *
     * <p>This method is invoked when the amount of bytes in the buffer appears to be insufficient
     * for {@link DataStoreProvider#probeContent(StorageConnector)} purpose.</p>
     *
     * @return {@code true} on success.
     * @throws DataStoreException If an error occurred while reading more bytes.
     */
    final boolean prefetch() throws DataStoreException {
        try {
            final ChannelDataInput c = getView(ChannelDataInput.class);
            if (c != null) {
                return c.prefetch() >= 0;
            }
            /*
             * The above code is the usual case. The code below this point is the fallback used when only
             * an ImageInputStream was available. In such case, the ByteBuffer can only be the one created
             * by the above createByteBuffer() method, which is known to be backed by a writable array.
             */
            final ImageInputStream input = getView(ImageInputStream.class);
            if (input != null) {
                final ByteBuffer buffer = getView(ByteBuffer.class);
                if (buffer != null) {
                    final int p = buffer.limit();
                    final int n = input.read(buffer.array(), p, buffer.capacity() - p);
                    if (n >= 0) {
                        buffer.limit(p + n);
                        return true;
                    }
                }
            }
        } catch (IOException e) {
            throw new DataStoreException(Errors.format(Errors.Keys.CanNotRead_1, getStorageName()), e);
        }
        return false;
    }

    /**
     * Creates a storage view of the given type if possible, or returns {@code null} otherwise.
     * This method is invoked by {@link #getStorageAs(Class)} when first needed, and the result is cached.
     *
     * @param  <T>  The compile-time type of the {@code type} argument.
     * @param  type The type of the view to create.
     * @return The storage as a view of the given type, or {@code null} if no view can be created for the given type.
     * @throws IllegalArgumentException If the given {@code type} argument is not a known type.
     * @throws Exception If an error occurred while opening a stream or database connection.
     */
    private Object createView(final Class<?> type) throws IllegalArgumentException, Exception {
        if (type == String.class) {
            return IOUtilities.toString(storage);
        }
        if (type == Connection.class) {
            if (storage instanceof Connection) {
                return storage;
            }
            if (storage instanceof DataSource) {
                final Connection c = ((DataSource) storage).getConnection();
                addViewToClose(c, storage);
                return c;
            }
            return null;
        }
        if (type == ImageInputStream.class) {
            final DataInput input = getStorageAs(DataInput.class);
            return (input instanceof ImageInputStream) ? input : null;
        }
        if (type == InputStream.class) {
            if (storage instanceof InputStream) {
                return storage;
            }
            final DataInput input = getStorageAs(DataInput.class);
            if (input instanceof InputStream) {
                return (InputStream) input;
            }
            if (input instanceof ImageInputStream) {
                final InputStream c = new InputStreamAdapter((ImageInputStream) input);
                addViewToClose(c, input);
                return c;
            }
            return null;
        }
        if (type == Reader.class) {
            if (storage instanceof Reader) {
                return storage;
            }
            final InputStream input = getStorageAs(InputStream.class);
            if (input != null) {
                final Charset encoding = getOption(OptionKey.ENCODING);
                final Reader c = (encoding != null) ? new InputStreamReader(input, encoding)
                                                    : new InputStreamReader(input);
                /*
                 * Current implementation does not wrap the above Reader in a BufferedReader because:
                 *
                 * 1) InputStreamReader already uses a buffer internally.
                 * 2) InputStreamReader does not support mark/reset, which is a desired limitation for now.
                 *    This is because reseting the Reader would not reset the underlying InputStream, which
                 *    would cause other DataStoreProvider.probeContent(…) methods to fail if they try to use
                 *    the InputStream. For now we let the InputStreamReader.mark() to throws an IOException,
                 *    but we may need to provide our own subclass of BufferedReader in a future SIS version
                 *    if mark/reset support is needed here.
                 */
                addViewToClose(c, input);
                return c;
            }
            return null;
        }
        throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, type));
    }

    /**
     * Adds the given view in the cache.
     *
     * @param <T>   The compile-time type of the {@code type} argument.
     * @param type  The view type.
     * @param view  The view, or {@code null} if none.
     */
    private <T> void addView(final Class<T> type, final T view) {
        if (views.put(type, (view != null) ? view : Void.TYPE) != null) {
            throw new ConcurrentModificationException();
        }
    }

    /**
     * Returns the view for the given type from the cache.
     *
     * @param <T>   The compile-time type of the {@code type} argument.
     * @param type  The view type.
     * @return      The view, or {@code null} if none.
     */
    private <T> T getView(final Class<T> type) {
        final Object view = views.get(type);
        return (view != Void.TYPE) ? type.cast(view) : null;
    }

    /**
     * Declares that the given {@code input} will need to be closed by the {@link #closeAllExcept(Object)} method.
     * The {@code input} argument is always a new instance wrapping, directly or indirectly, the {@link #storage}.
     * Callers must specify the wrapped object in the {@code delegate} argument.
     *
     * @param input    The newly created object which will need to be closed.
     * @param delegate The object wrapped by the given {@code input}.
     */
    private void addViewToClose(final Object input, final Object delegate) {
        if (viewsToClose == null) {
            viewsToClose = new IdentityHashMap<Object,Object>(4);
        }
        if (viewsToClose.put(input, delegate) != null) {
            throw new AssertionError(input);
        }
    }

    /**
     * Closes all streams and connections created by this {@code StorageConnector} except the given view.
     * This method closes all objects created by the {@link #getStorageAs(Class)} method except the given {@code view}.
     * If {@code view} is {@code null}, then this method closes everything including the {@linkplain #getStorage()
     * storage} if it is closeable.
     *
     * <p>This method is invoked when a suitable {@link DataStore} has been found - in which case the view used
     * by the data store is given in argument to this method - or when no suitable {@code DataStore} has been
     * found - in which case the {@code view} argument is null.</p>
     *
     * <p>This {@code StorageConnector} instance shall not be used anymore after invocation of this method.</p>
     *
     * @param  view The view to leave open, or {@code null} if none.
     * @throws DataStoreException If an error occurred while closing the stream or database connection.
     *
     * @see #getStorageAs(Class)
     * @see DataStoreProvider#open(StorageConnector)
     */
    public void closeAllExcept(final Object view) throws DataStoreException {
        final Map<Object,Object> toClose = viewsToClose;
        viewsToClose = Collections.emptyMap();
        views        = Collections.emptyMap();
        if (toClose == null) {
            if (storage != view && JDK7.isAutoCloseable(storage)) try {
                JDK7.close(storage);
            } catch (Exception e) {
                throw new DataStoreException(e);
            }
            return;
        }
        /*
         * The "AutoCloseable.close() is not indempotent" problem
         * ------------------------------------------------------
         * We will need a set of objects to close without duplicated values. For example the values associated to the
         * 'ImageInputStream.class' and 'DataInput.class' keys are often the same instance.  We must avoid duplicated
         * values because 'ImageInputStream.close()' is not indempotent,  i.e.  invoking their 'close()' method twice
         * will thrown an IOException.
         *
         * Generally speaking, all AutoCloseable instances are not guaranteed to be indempotent because this is not
         * required by the interface contract. Consequently we must be careful to not invoke the close() method on
         * the same instance twice (indirectly or indirectly).
         *
         * The set of objects to close will be the keys of the 'viewsToClose' map. It can not be the values of the
         * 'views' map.
         */
        toClose.put(storage, null);
        if (view != null) {
            /*
             * If there is a view to not close, search for all views that are wrapper for the given view.
             * Those wrappers shall not be closed. For example if the caller does not want to close the
             * InputStream view, then we shall not close the InputStreamReader wrapper neither.
             */
            final Queue<Object> deferred = new LinkedList<Object>();
            Object doNotClose = view;
            do {
                final Iterator<Map.Entry<Object,Object>> it = toClose.entrySet().iterator();
                while (it.hasNext()) {
                    final Map.Entry<Object,Object> entry = it.next();
                    if (entry.getValue() == doNotClose) {
                        deferred.add(entry.getKey());
                        it.remove();
                    }
                }
                doNotClose = deferred.poll();
            } while (doNotClose != null);
        }
        /*
         * Remove the view to not close. If that view is a wrapper for an other object, do not close the
         * wrapped object neither. Proceed the dependency chain up to the original 'storage' object.
         */
        for (Object doNotClose = view; doNotClose != null;) {
            doNotClose = toClose.remove(doNotClose);
        }
        /*
         * Remove all wrapped objects. After this loop, only the "top level" objects should remain
         * (typically only one object). This block is needed because of the "AutoCloseable.close()
         * is not idempotent" issue, otherwise we could have omitted it.
         */
        for (final Object delegate : toClose.values().toArray()) { // 'toArray()' is for avoiding ConcurrentModificationException.
            toClose.remove(delegate);
        }
        /*
         * Now close all remaining items. If an exception occurs, we will propagate it only after we are
         * done closing all items.
         */
        DataStoreException failure = null;
        for (final Object c : toClose.keySet()) {
            if (JDK7.isAutoCloseable(c)) try {
                JDK7.close(c);
            } catch (Exception e) {
                if (failure == null) {
                    failure = new DataStoreException(e);
                } else {
                    // failure.addSuppressed(e) on the JDK7 branch.
                }
            }
        }
        if (failure != null) {
            throw failure;
        }
    }

    /**
     * Returns a string representation of this {@code StorageConnector} for debugging purpose.
     */
    @Debug
    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder(40);
        buffer.append(Classes.getShortClassName(this)).append("[“").append(getStorageName()).append('”');
        if (options != null) {
            buffer.append(", options=").append(options);
        }
        return buffer.append(']').toString();
    }
}
TOP

Related Classes of org.apache.sis.storage.StorageConnector

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.