/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    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.
 *
 *    This package contains documentation from OpenGIS specifications.
 *    OpenGIS consortium's work is fully acknowledged here.
 */
package org.geotools.referencing.factory;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.Set;
import java.util.Iterator;
import java.util.Collection;
import java.util.Collections;
import java.util.WeakHashMap;
import java.util.LinkedHashMap;
import java.util.logging.LogRecord;
import java.util.logging.Level;
import javax.measure.unit.Unit;

import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.citation.Citation;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;
import org.opengis.util.InternationalString;

import org.geotools.factory.Hints;
import org.geotools.factory.BufferedFactory;
import org.geotools.util.Utilities;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Loggings;
import org.geotools.resources.i18n.LoggingKeys;


/**
 * An authority factory that caches all objects created by an other factory. All
 * {@code createFoo(String)} methods first looks if a previously created object
 * exists for the given code. If such an object exists, it is returned. Otherwise,
 * the object creation is delegated to the {@linkplain AbstractAuthorityFactory authority factory}
 * specified at creation time, and the result is cached in this buffered factory.
 * <p>
 * Objects are cached by strong references, up to the amount of objects specified at
 * construction time. If a greater amount of objects are cached, the oldest ones will
 * be retained through a {@linkplain WeakReference weak reference} instead of a strong
 * one. This means that this buffered factory will continue to returns them as long as
 * they are in use somewhere else in the Java virtual machine, but will be discarted
 * (and recreated on the fly if needed) otherwise.
 *
 * @since 2.1
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/referencing/factory/BufferedAuthorityFactory.java $
 * @version $Id: BufferedAuthorityFactory.java 37299 2011-05-25 05:21:24Z mbedward $
 * @author Martin Desruisseaux (IRD)
 */
public class BufferedAuthorityFactory extends AbstractAuthorityFactory implements BufferedFactory {
    /**
     * The default value for {@link #maxStrongReferences}.
     */
    static final int DEFAULT_MAX = 20;

    /**
     * The underlying authority factory. This field may be {@code null} if this object was
     * created by the {@linkplain #BufferedAuthorityFactory(AbstractAuthorityFactory,int)
     * package protected constructor}. In this case, the subclass is responsible for creating
     * the backing store when {@link DeferredAuthorityFactory#createBackingStore} is invoked.
     *
     * @see #getBackingStore
     * @see DeferredAuthorityFactory#createBackingStore
     */
    volatile AbstractAuthorityFactory backingStore;

    /**
     * The pool of cached objects.
     */
    private final LinkedHashMap<Object,Object> pool =
            new LinkedHashMap<Object,Object>(32, 0.75f, true);

    /**
     * The maximum number of objects to keep by strong reference. If a greater amount of
     * objects are created, then the strong references for the oldest ones are replaced by
     * weak references.
     */
    private final int maxStrongReferences;

    /**
     * The pool of objects identified by {@link #find}.
     */
    private final Map<IdentifiedObject,IdentifiedObject> findPool =
            new WeakHashMap<IdentifiedObject,IdentifiedObject>();

    /**
     * Constructs an instance without initial backing store. This constructor is for subclass
     * constructors only. Subclasses are responsible for creating an appropriate backing store
     * when the {@link DeferredAuthorityFactory#createBackingStore} method is invoked.
     *
     * @param priority The priority for this factory, as a number between
     *        {@link #MINIMUM_PRIORITY MINIMUM_PRIORITY} and
     *        {@link #MAXIMUM_PRIORITY MAXIMUM_PRIORITY} inclusive.
     * @param maxStrongReferences The maximum number of objects to keep by strong reference.
     *
     * @see DeferredAuthorityFactory#createBackingStore
     */
    BufferedAuthorityFactory(final int priority, final int maxStrongReferences) {
        super(priority);
        this.maxStrongReferences = maxStrongReferences;
        // completeHints() will be invoked by DeferredAuthorityFactory.getBackingStore()
    }

    /**
     * Completes the set of hints according the value currently set in this object. This method
     * is invoked by {@code BufferedAuthorityFactory} or by {@code DeferredAuthorityFactory} at
     * backing store creation time.
     * <p>
     * The backing store is of course an important dependency. This method gives a chance
     * to {@link org.geotools.factory.FactoryRegistry} to compare the user-requested hints
     * (especially {@link Hints#FORCE_LONGITUDE_FIRST_AXIS_ORDER}) against the backing store
     * hints, by following the dependency declared there.
     * <p>
     * DON'T FORGET to set those hints to {@code null} when {@link DeferredAuthorityFactory}
     * dispose the backing store.
     */
    final void completeHints() {
        if (backingStore instanceof DatumAuthorityFactory) {
            hints.put(Hints.DATUM_AUTHORITY_FACTORY, backingStore);
        }
        if (backingStore instanceof CSAuthorityFactory) {
            hints.put(Hints.CS_AUTHORITY_FACTORY, backingStore);
        }
        if (backingStore instanceof CRSAuthorityFactory) {
            hints.put(Hints.CRS_AUTHORITY_FACTORY, backingStore);
        }
        if (backingStore instanceof CoordinateOperationAuthorityFactory) {
            hints.put(Hints.COORDINATE_OPERATION_AUTHORITY_FACTORY, backingStore);
        }
    }

    /**
     * Returns the direct dependencies. The returned list contains the backing store
     * specified at construction time, or the exception if it can't be obtained.
     */
    @Override
    Collection<? super AuthorityFactory> dependencies() {
        Object factory;
        try {
            factory = getBackingStore();
        } catch (FactoryException e) {
            factory = e;
        }
        return Collections.singleton(factory);
    }

    /**
     * Returns the backing store authority factory.
     *
     * @return The backing store to uses in {@code createXXX(...)} methods.
     * @throws FactoryException if the creation of backing store failed.
     */
    AbstractAuthorityFactory getBackingStore() throws FactoryException {
        if (backingStore == null) {
            throw new FactoryException(Errors.format(ErrorKeys.DISPOSED_FACTORY));
        }
        return backingStore;
    }

    /**
     * Returns {@code true} if this factory is available. The default implementation returns
     * {@code false} if no backing store were setup and
     * {@link DeferredAuthorityFactory#createBackingStore} throws an exception.
     */
    @Override
    boolean isAvailable() {
        try {
            return getBackingStore().isAvailable();
        } catch (FactoryNotFoundException exception) {
            /*
             * The factory is not available. This is error may be normal; it happens
             * for example if no gt2-epsg-hsql.jar (or similar JAR) are found in the
             * classpath, which is the case for example in GeoServer 1.3. Do not log
             * any stack trace,  since stack traces suggest more serious errors than
             * what we really have here.
             */
        } catch (FactoryException exception) {
            /*
             * The factory creation failed for an other reason, which may be more
             * serious. Now it is time to log a warning with a stack trace.
             */
            final Citation citation = getAuthority();
            final Collection<? extends InternationalString> titles = citation.getAlternateTitles();
            InternationalString title = citation.getTitle();
            if (titles != null) {
                for (final InternationalString candidate : titles) {
                    /*
                     * Uses the longuest title instead of the main one. In Geotools
                     * implementation, the alternate title may contains usefull informations
                     * like the EPSG database version number and the database engine.
                     */
                    if (candidate.length() > title.length()) {
                        title  = candidate;
                    }
                }
            }
            final LogRecord record = Loggings.format(Level.WARNING,
                    LoggingKeys.UNAVAILABLE_AUTHORITY_FACTORY_$1, title);
            record.setSourceClassName(getClass().getName());
            record.setSourceMethodName("isAvailable");
            record.setThrown(exception);
            record.setLoggerName(LOGGER.getName());
            LOGGER.log(record);
        }
        return false;
    }

    /**
     * If this factory is a wrapper for the specified factory that do not add any additional
     * {@linkplain #getAuthorityCodes authority codes}, returns {@code true}. This method is
     * for {@link FallbackAuthorityFactory} internal use only and should not be public. A
     * cheap test without {@link #getBackingStore} invocation is suffisient for our needs.
     */
    @Override
    boolean sameAuthorityCodes(final AuthorityFactory factory) {
        final AbstractAuthorityFactory backingStore = this.backingStore; // Protect from changes.
        if (backingStore != null && backingStore.sameAuthorityCodes(factory)) {
            return true;
        }
        return super.sameAuthorityCodes(factory);
    }

    /**
     * Returns the vendor responsible for creating the underlying factory implementation.
     */
    @Override
    public Citation getVendor() {
        return (backingStore!=null) ? backingStore.getVendor() : super.getVendor();
    }

    /**
     * Returns the organization or party responsible for definition and maintenance of the
     * underlying database.
     */
    public Citation getAuthority() {
        return (backingStore!=null) ? backingStore.getAuthority() : null;
    }

    /**
     * Returns a description of the underlying backing store, or {@code null} if unknow.
     * This is for example the database software used for storing the data.
     *
     * @throws FactoryException if a failure occured while fetching the engine description.
     */
    @Override
    public String getBackingStoreDescription() throws FactoryException {
        return getBackingStore().getBackingStoreDescription();
    }

    /**
     * Returns the set of authority codes of the given type. The {@code type}
     * argument specify the base class.
     *
     * @param  type The spatial reference objects type.
     * @return The set of authority codes for spatial reference objects of the given type.
     *         If this factory doesn't contains any object of the given type, then this method
     *         returns an {@linkplain java.util.Collections#EMPTY_SET empty set}.
     * @throws FactoryException if access to the underlying database failed.
     */
    public Set<String> getAuthorityCodes(final Class<? extends IdentifiedObject> type)
            throws FactoryException
    {
        return getBackingStore().getAuthorityCodes(type);
    }

    /**
     * Gets a description of the object corresponding to a code.
     *
     * @param  code Value allocated by authority.
     * @return A description of the object, or {@code null} if the object
     *         corresponding to the specified {@code code} has no description.
     * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
     * @throws FactoryException if the query failed for some other reason.
     */
    public InternationalString getDescriptionText(final String code)
            throws NoSuchAuthorityCodeException, FactoryException
    {
        return getBackingStore().getDescriptionText(code);
    }

    /**
     * Returns an arbitrary object from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized IdentifiedObject createObject(final String code)
            throws FactoryException
    {
        final IdentifiedObject object;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof IdentifiedObject) {
            object = (IdentifiedObject) cached;
        } else {
            object = getBackingStore().createObject(code);
        }
        put(key, object);
        return object;
    }

    /**
     * Returns an arbitrary datum from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized Datum createDatum(final String code)
            throws FactoryException
    {
        final Datum datum;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof Datum) {
            datum = (Datum) cached;
        } else {
            datum = getBackingStore().createDatum(code);
        }
        put(key, datum);
        return datum;
    }

    /**
     * Returns an engineering datum from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized EngineeringDatum createEngineeringDatum(final String code)
            throws FactoryException
    {
        final EngineeringDatum datum;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof EngineeringDatum) {
            datum = (EngineeringDatum) cached;
        } else {
            datum = getBackingStore().createEngineeringDatum(code);
        }
        put(key, datum);
        return datum;
    }

    /**
     * Returns an image datum from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized ImageDatum createImageDatum(final String code)
            throws FactoryException
    {
        final ImageDatum datum;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof ImageDatum) {
            datum = (ImageDatum) cached;
        } else {
            datum = getBackingStore().createImageDatum(code);
        }
        put(key, datum);
        return datum;
    }

    /**
     * Returns a vertical datum from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized VerticalDatum createVerticalDatum(final String code)
            throws FactoryException
    {
        final VerticalDatum datum;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof VerticalDatum) {
            datum = (VerticalDatum) cached;
        } else {
            datum = getBackingStore().createVerticalDatum(code);
        }
        put(key, datum);
        return datum;
    }

    /**
     * Returns a temporal datum from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized TemporalDatum createTemporalDatum(final String code)
            throws FactoryException
    {
        final TemporalDatum datum;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof TemporalDatum) {
            datum = (TemporalDatum) cached;
        } else {
            datum = getBackingStore().createTemporalDatum(code);
        }
        put(key, datum);
        return datum;
    }

    /**
     * Returns a geodetic datum from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized GeodeticDatum createGeodeticDatum(final String code)
            throws FactoryException
    {
        final GeodeticDatum datum;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof GeodeticDatum) {
            datum = (GeodeticDatum) cached;
        } else {
            datum = getBackingStore().createGeodeticDatum(code);
        }
        put(key, datum);
        return datum;
    }

    /**
     * Returns an ellipsoid from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized Ellipsoid createEllipsoid(final String code)
            throws FactoryException
    {
        final Ellipsoid ellipsoid;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof Ellipsoid) {
            ellipsoid = (Ellipsoid) cached;
        } else {
            ellipsoid = getBackingStore().createEllipsoid(code);
        }
        put(key, ellipsoid);
        return ellipsoid;
    }

    /**
     * Returns a prime meridian from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized PrimeMeridian createPrimeMeridian(final String code)
            throws FactoryException
    {
        final PrimeMeridian meridian;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof PrimeMeridian) {
            meridian = (PrimeMeridian) cached;
        } else {
            meridian = getBackingStore().createPrimeMeridian(code);
        }
        put(key, meridian);
        return meridian;
    }

    /**
     * Returns an extent (usually an area of validity) from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized Extent createExtent(final String code)
            throws FactoryException
    {
        final Extent extent;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof Extent) {
            extent = (Extent) cached;
        } else {
            extent = getBackingStore().createExtent(code);
        }
        put(key, extent);
        return extent;
    }

    /**
     * Returns an arbitrary coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized CoordinateSystem createCoordinateSystem(final String code)
            throws FactoryException
    {
        final CoordinateSystem cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CoordinateSystem) {
            cs = (CoordinateSystem) cached;
        } else {
            cs = getBackingStore().createCoordinateSystem(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a cartesian coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized CartesianCS createCartesianCS(final String code)
            throws FactoryException
    {
        final CartesianCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CartesianCS) {
            cs = (CartesianCS) cached;
        } else {
            cs = getBackingStore().createCartesianCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a polar coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized PolarCS createPolarCS(final String code)
            throws FactoryException
    {
        final PolarCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof PolarCS) {
            cs = (PolarCS) cached;
        } else {
            cs = getBackingStore().createPolarCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a cylindrical coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized CylindricalCS createCylindricalCS(final String code)
            throws FactoryException
    {
        final CylindricalCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CylindricalCS) {
            cs = (CylindricalCS) cached;
        } else {
            cs = getBackingStore().createCylindricalCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a spherical coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized SphericalCS createSphericalCS(final String code)
            throws FactoryException
    {
        final SphericalCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof SphericalCS) {
            cs = (SphericalCS) cached;
        } else {
            cs = getBackingStore().createSphericalCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns an ellipsoidal coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized EllipsoidalCS createEllipsoidalCS(final String code)
            throws FactoryException
    {
        final EllipsoidalCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof EllipsoidalCS) {
            cs = (EllipsoidalCS) cached;
        } else {
            cs = getBackingStore().createEllipsoidalCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a vertical coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized VerticalCS createVerticalCS(final String code)
            throws FactoryException
    {
        final VerticalCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof VerticalCS) {
            cs = (VerticalCS) cached;
        } else {
            cs = getBackingStore().createVerticalCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a temporal coordinate system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized TimeCS createTimeCS(final String code)
            throws FactoryException
    {
        final TimeCS cs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof TimeCS) {
            cs = (TimeCS) cached;
        } else {
            cs = getBackingStore().createTimeCS(code);
        }
        put(key, cs);
        return cs;
    }

    /**
     * Returns a coordinate system axis from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized CoordinateSystemAxis createCoordinateSystemAxis(final String code)
            throws FactoryException
    {
        final CoordinateSystemAxis axis;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CoordinateSystemAxis) {
            axis = (CoordinateSystemAxis) cached;
        } else {
            axis = getBackingStore().createCoordinateSystemAxis(code);
        }
        put(key, axis);
        return axis;
    }

    /**
     * Returns an unit from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized Unit<?> createUnit(final String code)
            throws FactoryException
    {
        final Unit<?> unit;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof Unit) {
            unit = (Unit) cached;
        } else {
            unit = getBackingStore().createUnit(code);
        }
        put(key, unit);
        return unit;
    }

    /**
     * Returns an arbitrary coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized CoordinateReferenceSystem createCoordinateReferenceSystem(final String code)
            throws FactoryException
    {
        final CoordinateReferenceSystem crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CoordinateReferenceSystem) {
            crs = (CoordinateReferenceSystem) cached;
        } else {
            crs = getBackingStore().createCoordinateReferenceSystem(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a 3D coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized CompoundCRS createCompoundCRS(final String code)
            throws FactoryException
    {
        final CompoundCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CompoundCRS) {
            crs = (CompoundCRS) cached;
        } else {
            crs = getBackingStore().createCompoundCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a derived coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized DerivedCRS createDerivedCRS(final String code)
            throws FactoryException
    {
        final DerivedCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof DerivedCRS) {
            crs = (DerivedCRS) cached;
        } else {
            crs = getBackingStore().createDerivedCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns an engineering coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized EngineeringCRS createEngineeringCRS(final String code)
            throws FactoryException
    {
        final EngineeringCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof EngineeringCRS) {
            crs = (EngineeringCRS) cached;
        } else {
            crs = getBackingStore().createEngineeringCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a geographic coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized GeographicCRS createGeographicCRS(final String code)
            throws FactoryException
    {
        final GeographicCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof GeographicCRS) {
            crs = (GeographicCRS) cached;
        } else {
            crs = getBackingStore().createGeographicCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a geocentric coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized GeocentricCRS createGeocentricCRS(final String code)
            throws FactoryException
    {
        final GeocentricCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof GeocentricCRS) {
            crs = (GeocentricCRS) cached;
        } else {
            crs = getBackingStore().createGeocentricCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns an image coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized ImageCRS createImageCRS(final String code)
            throws FactoryException
    {
        final ImageCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof ImageCRS) {
            crs = (ImageCRS) cached;
        } else {
            crs = getBackingStore().createImageCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a projected coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized ProjectedCRS createProjectedCRS(final String code)
            throws FactoryException
    {
        final ProjectedCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof ProjectedCRS) {
            crs = (ProjectedCRS) cached;
        } else {
            crs = getBackingStore().createProjectedCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a temporal coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized TemporalCRS createTemporalCRS(final String code)
            throws FactoryException
    {
        final TemporalCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof TemporalCRS) {
            crs = (TemporalCRS) cached;
        } else {
            crs = getBackingStore().createTemporalCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a vertical coordinate reference system from a code.
     *
     * @throws FactoryException if the object creation failed.
     */
    @Override
    public synchronized VerticalCRS createVerticalCRS(final String code)
            throws FactoryException
    {
        final VerticalCRS crs;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof VerticalCRS) {
            crs = (VerticalCRS) cached;
        } else {
            crs = getBackingStore().createVerticalCRS(code);
        }
        put(key, crs);
        return crs;
    }

    /**
     * Returns a parameter descriptor from a code.
     *
     * @throws FactoryException if the object creation failed.
     *
     * @since 2.2
     */
    @Override
    public synchronized ParameterDescriptor createParameterDescriptor(final String code)
            throws FactoryException
    {
        final ParameterDescriptor parameter;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof ParameterDescriptor) {
            parameter = (ParameterDescriptor) cached;
        } else {
            parameter = getBackingStore().createParameterDescriptor(code);
        }
        put(key, parameter);
        return parameter;
    }

    /**
     * Returns an operation method from a code.
     *
     * @throws FactoryException if the object creation failed.
     *
     * @since 2.2
     */
    @Override
    public synchronized OperationMethod createOperationMethod(final String code)
            throws FactoryException
    {
        final OperationMethod method;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof OperationMethod) {
            method = (OperationMethod) cached;
        } else {
            method = getBackingStore().createOperationMethod(code);
        }
        put(key, method);
        return method;
    }

    /**
     * Returns an operation from a single operation code.
     *
     * @throws FactoryException if the object creation failed.
     *
     * @since 2.2
     */
    @Override
    public synchronized CoordinateOperation createCoordinateOperation(final String code)
            throws FactoryException
    {
        final CoordinateOperation operation;
        final String key = trimAuthority(code);
        final Object cached = get(key);
        if (cached instanceof CoordinateOperation) {
            operation = (CoordinateOperation) cached;
        } else {
            operation = getBackingStore().createCoordinateOperation(code);
        }
        put(key, operation);
        return operation;
    }

    /**
     * Returns an operation from coordinate reference system codes.
     *
     * @throws FactoryException if the object creation failed.
     *
     * @since 2.2
     */
    @Override
    public synchronized Set<CoordinateOperation> createFromCoordinateReferenceSystemCodes(
                        final String sourceCRS, final String targetCRS)
            throws FactoryException
    {
        final Set<CoordinateOperation> operations;
        final CodePair key = new CodePair(trimAuthority(sourceCRS), trimAuthority(targetCRS));
        final Object cached = get(key);
        if (cached instanceof Set) {
            operations = (Set<CoordinateOperation>) cached;
        } else {
            operations = Collections.unmodifiableSet(getBackingStore()
                         .createFromCoordinateReferenceSystemCodes(sourceCRS, targetCRS));
        }
        put(key, operations);
        return operations;
    }

    /**
     * A pair of codes for operations to cache with
     * {@link #createFromCoordinateReferenceSystemCodes}.
     */
    private static final class CodePair {
        private final String source, target;

        public CodePair(final String source, final String target) {
            this.source = source;
            this.target = target;
        }

        @Override
        public int hashCode() {
            int code = 0;
            if (source != null) code  = source.hashCode();
            if (target != null) code += target.hashCode() * 37;
            return code;
        }

        @Override
        public boolean equals(final Object other) {
            if (other instanceof CodePair) {
                final CodePair that = (CodePair) other;
                return Utilities.equals(this.source, that.source) &&
                       Utilities.equals(this.target, that.target);
            }
            return false;
        }

        @Override
        public String toString() {
            return source + " \u21E8 " + target;
        }
    }

    /**
     * Returns a finder which can be used for looking up unidentified objects.
     * The default implementation delegates lookup to the underlying backing
     * store and caches the result.
     *
     * @throws FactoryException if the finder can not be created.
     *
     * @since 2.4
     */
    @Override
    public synchronized IdentifiedObjectFinder getIdentifiedObjectFinder(
            final Class<? extends IdentifiedObject> type) throws FactoryException
    {
        return new Finder(getBackingStore().getIdentifiedObjectFinder(type));
    }

    /**
     * An implementation of {@link IdentifiedObjectFinder} which delegates
     * the work to the underlying backing store and caches the result.
     * <p>
     * <b>Implementation note:</b> we will create objects using directly the underlying backing
     * store, not using the cache. This is because hundred of objects may be created during a
     * scan while only one will be typically retained. We don't want to overload the cache with
     * every false candidates that we encounter during the scan.
     */
    private final class Finder extends IdentifiedObjectFinder.Adapter {
        /**
         * Creates a finder for the underlying backing store.
         */
        Finder(final IdentifiedObjectFinder finder) {
            super(finder);
        }

        /**
         * Looks up an object from this authority factory which is equals, ignoring metadata,
         * to the specified object. The default implementation performs the same lookup than
         * the backing store and caches the result.
         */
        @Override
        public IdentifiedObject find(final IdentifiedObject object) throws FactoryException {
            /*
             * Do not synchronize on 'BufferedAuthorityFactory.this'. This method may take a
             * while to execute and we don't want to block other threads. The synchronizations
             * in the 'create' methods and in the 'findPool' map should be suffisient.
             *
             * TODO: avoid to search for the same object twice. For now we consider that this
             *       is not a big deal if the same object is searched twice; it is "just" a
             *       waste of CPU.
             */
            IdentifiedObject candidate;
            synchronized (findPool) {
                candidate = findPool.get(object);
            }
            if (candidate == null) {
                // Must delegates to 'finder' (not to 'super') in order to take
                // advantage of the method overriden by AllAuthoritiesFactory.
                candidate = finder.find(object);
                if (candidate != null) {
                    synchronized (findPool) {
                        findPool.put(object, candidate);
                    }
                }
            }
            return candidate;
        }

        /**
         * Returns the identifier for the specified object.
         */
        @Override
        public String findIdentifier(final IdentifiedObject object) throws FactoryException {
            IdentifiedObject candidate;
            synchronized (findPool) {
                candidate = findPool.get(object);
            }
            if (candidate != null) {
                return getIdentifier(candidate);
            }
            // We don't rely on super-class implementation, because we want to
            // take advantage of the method overriden by AllAuthoritiesFactory.
            return finder.findIdentifier(object);
        }
    }

    /**
     * Releases resources immediately instead of waiting for the garbage collector.
     *
     * @throws FactoryException if an error occured while disposing the factory.
     */
    @Override
    public synchronized void dispose() throws FactoryException {
        if (backingStore != null) {
            backingStore.dispose();
            backingStore = null;
        }
        pool.clear();
        findPool.clear();
        super.dispose();
    }

    /**
     * Returns an object from the pool for the specified code. If the object was retained as a
     * {@linkplain Reference weak reference}, the {@link Reference#get referent} is returned.
     *
     * @todo Consider logging a message here to the finer or finest level.
     */
    private Object get(final Object key) {
        assert Thread.holdsLock(this);
        Object object = pool.get(key);
        if (object instanceof Reference) {
            object = ((Reference<?>) object).get();
        }
        return object;
    }

    /**
     * Put an element in the pool. This method is invoked everytime a {@code createFoo(...)}
     * method is invoked, even if an object was already in the pool for the given code, for
     * the following reasons: 1) Replaces weak reference by strong reference (if applicable)
     * and 2) Alters the linked hash set order, so that this object is declared as the last
     * one used.
     */
    private void put(final Object key, final Object object) {
        assert Thread.holdsLock(this);
        pool.put(key, object);
        int toReplace = pool.size() - maxStrongReferences;
        if (toReplace > 0) {
            for (final Iterator<Map.Entry<Object,Object>> it=pool.entrySet().iterator(); it.hasNext();) {
                final Map.Entry<Object,Object> entry = it.next();
                final Object value = entry.getValue();
                if (value instanceof Reference) {
                    if (((Reference) value).get() == null) {
                        it.remove();
                    }
                    continue;
                }
                entry.setValue(new WeakReference<Object>(value));
                if (--toReplace == 0) {
                    break;
                }
            }
        }
    }
}
