This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit c456f0598daa5ce46ea42392556c207b1911f03d Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Dec 7 20:18:34 2023 +0100 Add an option to complete the metadata of a resource with an auxiliary metadata file. --- .../org/apache/sis/storage/gpx/WritableStore.java | 2 +- .../main/org/apache/sis/storage/DataOptionKey.java | 18 ++-- .../apache/sis/storage/base/MetadataBuilder.java | 57 +++++++++- .../org/apache/sis/storage/base/PRJDataStore.java | 38 +++---- .../org/apache/sis/storage/base/URIDataStore.java | 116 ++++++++++++--------- .../main/org/apache/sis/storage/csv/Store.java | 1 + .../org/apache/sis/storage/csv/StoreProvider.java | 2 +- .../org/apache/sis/storage/esri/RasterStore.java | 4 +- .../apache/sis/storage/esri/RawRasterStore.java | 3 +- .../apache/sis/storage/image/WorldFileStore.java | 15 +-- .../apache/sis/storage/image/WritableStore.java | 7 ++ .../org/apache/sis/storage/internal/Resources.java | 5 + .../sis/storage/internal/Resources.properties | 1 + .../sis/storage/internal/Resources_fr.properties | 1 + .../main/org/apache/sis/storage/wkt/Store.java | 2 + .../main/org/apache/sis/storage/xml/Store.java | 1 + .../main/org/apache/sis/setup/OptionKey.java | 14 ++- .../main/org/apache/sis/setup/package-info.java | 2 +- .../main/org/apache/sis/util/ArraysExt.java | 83 +++++++++++++-- .../apache/sis/util/internal/CollectionsExt.java | 16 +++ .../test/org/apache/sis/util/ArraysExtTest.java | 26 +++-- 21 files changed, 298 insertions(+), 116 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java index a0c358419f..71a4317fef 100644 --- a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java +++ b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java @@ -126,7 +126,7 @@ public final class WritableStore extends Store implements WritableFeatureSet { */ private Updater updater() throws DataStoreException { try { - return new Updater(this, getSpecifiedPath()); + return new Updater(this, locationAsPath); } catch (IOException e) { throw new DataStoreException(e); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java index 87a642be2c..89ef371110 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java @@ -16,7 +16,7 @@ */ package org.apache.sis.storage; -import org.opengis.referencing.crs.CoordinateReferenceSystem; +import java.nio.file.Path; import org.apache.sis.setup.OptionKey; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.feature.FoliationRepresentation; @@ -28,7 +28,7 @@ import org.apache.sis.feature.FoliationRepresentation; * or other kinds of structure that are specific to some data formats. * * @author Martin Desruisseaux (Geomatys) - * @version 1.3 + * @version 1.5 * * @param <T> the type of option values. * @@ -41,15 +41,15 @@ public final class DataOptionKey<T> extends OptionKey<T> { private static final long serialVersionUID = 8927757348322016043L; /** - * The coordinate reference system (CRS) of data to use if not explicitly defined. - * This option can be used when the file to read does not describe itself the data CRS. - * For example, this option can be used when reading ASCII Grid without CRS information, - * but is ignored if the ASCII Grid file is accompanied by a {@code *.prj} file giving the CRS. + * Path to an auxiliary file containing metadata encoded in an ISO 19115-3 XML document. + * The given path, if not absolute, is relative to the path of the main storage file. + * If the file exists, it is parsed and its content is merged or appended after the + * metadata read by the storage. If the file does not exist, then it is ignored. * - * @since 1.2 + * @since 1.5 */ - public static final OptionKey<CoordinateReferenceSystem> DEFAULT_CRS = - new DataOptionKey<>("DEFAULT_CRS", CoordinateReferenceSystem.class); + public static final OptionKey<Path> METADATA_PATH = + new DataOptionKey<>("METADATA_PATH", Path.class); /** * Whether to assemble trajectory fragments (distinct CSV lines) into a single {@code Feature} instance diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java index 2e72d01100..9236535864 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java @@ -29,6 +29,7 @@ import java.util.LinkedHashSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.net.URI; import java.nio.charset.Charset; import javax.measure.Unit; @@ -3302,7 +3303,7 @@ parse: for (int i = 0; i < length;) { } /** - * Merge the given metadata into the metadata created by this builder. + * Merges the given metadata into the metadata created by this builder. * The given source should be an instance of {@link Metadata}, * but some types of metadata components are accepted as well. * @@ -3346,9 +3347,63 @@ parse: for (int i = 0; i < length;) { else return false; final Merger merger = new Merger(locale); merger.copy(source, target); + useParentElements(); return true; } + /** + * Replaces any null metadata element by the last element from the parent. + * This is used for continuing the edition of an existing metadata. + */ + private void useParentElements() { + if (identification == null) identification = last (DefaultDataIdentification.class, metadata, Metadata::getIdentificationInfo); + if (citation == null) citation = fetch(DefaultCitation.class, identification, Identification::getCitation); + if (responsibility == null) responsibility = last (DefaultResponsibility.class, citation, Citation::getCitedResponsibleParties); + if (party == null) party = last (AbstractParty.class, responsibility, Responsibility::getParties); + if (constraints == null) constraints = last (DefaultLegalConstraints.class, identification, Identification::getResourceConstraints); + if (extent == null) extent = last (DefaultExtent.class, identification, Identification::getExtents); + if (acquisition == null) acquisition = last (DefaultAcquisitionInformation.class, metadata, Metadata::getAcquisitionInformation); + if (platform == null) platform = last (DefaultPlatform.class, acquisition, AcquisitionInformation::getPlatforms); + } + + /** + * Returns the element of the given source metadata if it is of the desired class. + * This method is equivalent to {@link #last(Class, Object, Function)} but for a singleton. + * + * @param target the desired class. + * @param source the source metadata, or {@code null} if none. + * @param getter the getter to use for fetching elements from the source metadata. + * @return the metadata element from the source, or {@code null} if none. + */ + private static <S,T> T fetch(final Class<T> target, final S source, final Function<S,?> getter) { + if (source != null) { + final Object last = getter.apply(source); + if (target.isInstance(last)) { + return target.cast(last); + } + } + return null; + } + + /** + * Returns the element of the given source metadata if it is of the desired class. + * This method is equivalent to {@link #fetch(Class, Object, Function)} but for a collection. + * + * @param target the desired class. + * @param source the source metadata, or {@code null} if none. + * @param getter the getter to use for fetching elements from the source metadata. + * @return the metadata element from the source, or {@code null} if none. + */ + private static <S,T> T last(final Class<T> target, final S source, final Function<S,Collection<?>> getter) { + if (source != null) { + final Object last = CollectionsExt.last(getter.apply(source)); + if (target.isInstance(last)) { + return target.cast(last); + } + } + return null; + } + /** * Writes all pending metadata objects into the {@link DefaultMetadata} root class. * Then all {@link #identification}, {@link #gridRepresentation}, <i>etc.</i> fields diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java index 2f9d380e0c..5bd4edcebd 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java @@ -39,7 +39,7 @@ import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.apache.sis.storage.DataOptionKey; +import org.apache.sis.setup.OptionKey; import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreProvider; import org.apache.sis.storage.DataStoreException; @@ -60,13 +60,13 @@ import org.apache.sis.util.resources.Vocabulary; /** * A data store for a file or URI accompanied by an auxiliary file of the same name with {@code .prj} extension. - * If the auxiliary file is absent, {@link DataOptionKey#DEFAULT_CRS} is used as a fallback. + * If the auxiliary file is absent, {@link OptionKey#DEFAULT_CRS} is used as a fallback. * The WKT 1 variant used for parsing the {@code "*.prj"} file is the variant used by "World Files" and GDAL; * this is not the standard specified by OGC 01-009 (they differ in there interpretation of units of measurement). * * <p>It is still possible to create a data store with a {@link java.nio.channels.ReadableByteChannel}, * {@link java.io.InputStream} or {@link java.io.Reader}, in which case the {@linkplain #location} will - * be null and the CRS defined by the {@code DataOptionKey} will be used.</p> + * be null and the CRS defined by the {@code OptionKey} will be used.</p> * * @author Martin Desruisseaux (Geomatys) */ @@ -105,7 +105,7 @@ public abstract class PRJDataStore extends URIDataStore { private final TimeZone timezone; /** - * The coordinate reference system. This is initialized on the value provided by {@link DataOptionKey#DEFAULT_CRS} + * The coordinate reference system. This is initialized on the value provided by {@link OptionKey#DEFAULT_CRS} * at construction time, and is modified later if a {@code "*.prj"} file is found. */ protected CoordinateReferenceSystem crs; @@ -116,10 +116,10 @@ public abstract class PRJDataStore extends URIDataStore { * * <p>The following options are recognized:</p> * <ul> - * <li>{@link DataOptionKey#DEFAULT_CRS}: default CRS if no auxiliary {@code "*.prj"} file is found.</li> - * <li>{@link DataOptionKey#ENCODING}: encoding of the {@code "*.prj"} file. Default is the JVM default.</li> - * <li>{@link DataOptionKey#TIMEZONE}: timezone of dates in the {@code "*.prj"} file. Default is UTC.</li> - * <li>{@link DataOptionKey#LOCALE}: locale for texts in the {@code "*.prj"} file. Default is English.</li> + * <li>{@link OptionKey#DEFAULT_CRS}: default CRS if no auxiliary {@code "*.prj"} file is found.</li> + * <li>{@link OptionKey#ENCODING}: encoding of the {@code "*.prj"} file. Default is the JVM default.</li> + * <li>{@link OptionKey#TIMEZONE}: timezone of dates in the {@code "*.prj"} file. Default is UTC.</li> + * <li>{@link OptionKey#LOCALE}: locale for texts in the {@code "*.prj"} file. Default is English.</li> * </ul> * * @param provider the factory that created this {@code PRJDataStore} instance, or {@code null} if unspecified. @@ -128,10 +128,10 @@ public abstract class PRJDataStore extends URIDataStore { */ protected PRJDataStore(final DataStoreProvider provider, final StorageConnector connector) throws DataStoreException { super(provider, connector); - crs = connector.getOption(DataOptionKey.DEFAULT_CRS); - encoding = connector.getOption(DataOptionKey.ENCODING); - locale = connector.getOption(DataOptionKey.LOCALE); // For `InternationalString`, not for numbers. - timezone = connector.getOption(DataOptionKey.TIMEZONE); + crs = connector.getOption(OptionKey.DEFAULT_CRS); + encoding = connector.getOption(OptionKey.ENCODING); + locale = connector.getOption(OptionKey.LOCALE); // For `InternationalString`, not for numbers. + timezone = connector.getOption(OptionKey.TIMEZONE); } /** @@ -148,7 +148,7 @@ public abstract class PRJDataStore extends URIDataStore { try { final AuxiliaryContent content = readAuxiliaryFile(PRJ); if (content == null) { - listeners.warning(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, PRJ)); + listeners.warning(cannotReadAuxiliaryFile(PRJ)); return; } final String wkt = content.toString(); @@ -166,12 +166,12 @@ public abstract class PRJDataStore extends URIDataStore { return; } } catch (NoSuchFileException | FileNotFoundException e) { - listeners.warning(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, PRJ), e); + listeners.warning(cannotReadAuxiliaryFile(PRJ), e); return; } catch (IOException | ParseException | ClassCastException e) { cause = e; } - throw new DataStoreReferencingException(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, PRJ), cause); + throw new DataStoreReferencingException(cannotReadAuxiliaryFile(PRJ), cause); } /** @@ -196,7 +196,7 @@ public abstract class PRJDataStore extends URIDataStore { * and URL does not open S3 files in current implementation. */ final InputStream stream; - Path path = getSpecifiedPath(); + Path path = locationAsPath; final Object source; // In case an error message is produced. if (path != null) { final String base = getBaseFilename(path); @@ -212,7 +212,7 @@ public abstract class PRJDataStore extends URIDataStore { stream = url.openStream(); source = url; } catch (URISyntaxException e) { - throw new DataStoreException(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, "*." + extension), e); + throw new DataStoreException(cannotReadAuxiliaryFile(extension), e); } /* * Reads the auxiliary file fully, with an arbitrary size limit. @@ -495,7 +495,7 @@ public abstract class PRJDataStore extends URIDataStore { */ @Override protected ParameterDescriptorGroup build(final ParameterBuilder builder) { - return builder.createGroup(LOCATION_PARAM, DEFAULT_CRS); + return builder.createGroup(LOCATION_PARAM, METADATA_PARAM, DEFAULT_CRS); } /** @@ -509,7 +509,7 @@ public abstract class PRJDataStore extends URIDataStore { ArgumentChecks.ensureNonNull("parameter", parameters); final StorageConnector connector = connector(this, parameters); final Parameters pg = Parameters.castOrWrap(parameters); - connector.setOption(DataOptionKey.DEFAULT_CRS, pg.getValue(DEFAULT_CRS)); + connector.setOption(OptionKey.DEFAULT_CRS, pg.getValue(DEFAULT_CRS)); return open(connector); } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java index f59126c0d3..df7f1141ae 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java @@ -21,14 +21,14 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.InputStream; import java.io.OutputStream; -import java.io.File; +import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; -import java.nio.file.FileSystemNotFoundException; import java.nio.charset.Charset; +import jakarta.xml.bind.JAXBException; import org.opengis.util.GenericName; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptor; @@ -36,6 +36,7 @@ import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterNotFoundException; import org.apache.sis.parameter.ParameterBuilder; import org.apache.sis.storage.StorageConnector; +import org.apache.sis.storage.DataOptionKey; import org.apache.sis.storage.Resource; import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreProvider; @@ -47,6 +48,7 @@ import org.apache.sis.setup.OptionKey; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.iso.Names; import org.apache.sis.util.logging.Logging; +import org.apache.sis.xml.XML; /** @@ -64,23 +66,17 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R protected final URI location; /** - * The {@link #location} as a path, computed when first needed. - * If the storage given at construction time was a {@link Path} or a {@link File} instance, - * then this field is initialized in the constructor in order to avoid a "path → URI → path" roundtrip - * (such roundtrip transforms relative paths into {@linkplain Path#toAbsolutePath() absolute paths}). + * The {@link #location} as a path, or {@code null} if none or if the URI cannot be converted to a path. * - * @see #getSpecifiedPath() * @see #getComponentFiles() */ - private volatile Path locationAsPath; + protected final Path locationAsPath; /** - * Whether {@link #locationAsPath} was initialized at construction time ({@code true}) - * of inferred from the {@link #location} URI at a later time ({@code false}). - * - * @see #getSpecifiedPath() + * Path to an auxiliary file providing metadata as path, or {@code null} if none or not applicable. + * Unless absolute, this path is relative to the {@link #location} or to the {@link #locationAsPath}. */ - private final boolean locationIsPath; + private final Path metadataPath; /** * Creates a new data store. This constructor does not open the file, @@ -94,16 +90,13 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R */ protected URIDataStore(final DataStoreProvider provider, final StorageConnector connector) throws DataStoreException { super(provider, connector); - location = connector.getStorageAs(URI.class); - final Object storage = connector.getStorage(); - if (storage instanceof Path) { - locationAsPath = (Path) storage; - } else if (storage instanceof File) { - locationAsPath = ((File) storage).toPath(); - } else if (storage instanceof CharSequence) { - locationAsPath = connector.getStorageAs(Path.class); + location = connector.getStorageAs(URI.class); + locationAsPath = connector.getStorageAs(Path.class); + if (locationAsPath != null || location != null) { + metadataPath = connector.getOption(DataOptionKey.METADATA_PATH); + } else { + metadataPath = null; } - locationIsPath = (locationAsPath != null); } /** @@ -141,38 +134,31 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R } /** - * If the location was specified as a {@link Path} or {@link File} instance, returns that path. - * Otherwise returns {@code null}. This method does not try to convert URI to {@link Path} - * because this conversion may fail for HTTP and FTP connections. - * - * @return the path specified at construction time, or {@code null} if the storage was not specified as a path. + * Returns the path to the auxiliary metadata file, or {@code null} if none. + * This is a path build from the "metadata path" option if present. */ - protected final Path getSpecifiedPath() { - return locationIsPath ? locationAsPath : null; + private Path getMetadataPath() { + if (metadataPath != null && locationAsPath != null) { + Path path = locationAsPath.getParent(); + if (path != null) { + return path.resolve(metadataPath); + } + } + return null; } /** - * Returns the {@linkplain #location} as a {@code Path} component or an empty array if none. - * The default implementation returns the storage specified at construction time if it was - * a {@link Path} or {@link File}, or converts the URI to a {@link Path} otherwise. + * Returns the main and metadata locations as {@code Path} components, or an empty array if none. + * The default implementation returns the storage specified at construction time converted to a {@link Path} + * if such conversion was possible, or {@code null} otherwise. * * @return the URI as a path, or an empty array if unknown. * @throws DataStoreException if the URI cannot be converted to a {@link Path}. */ @Override public Path[] getComponentFiles() throws DataStoreException { - Path path = locationAsPath; - if (path == null) { - if (location == null) { - return new Path[0]; - } else try { - path = Path.of(location); - } catch (IllegalArgumentException | FileSystemNotFoundException e) { - throw new DataStoreException(e); - } - locationAsPath = path; - } - return new Path[] {path}; + final var paths = new Path[] {locationAsPath, getMetadataPath()}; + return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, ArraysExt.removeNulls(paths))); } /** @@ -216,6 +202,11 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R */ public static final ParameterDescriptor<URI> LOCATION_PARAM; + /** + * Description of the "metadata" parameter. + */ + public static final ParameterDescriptor<Path> METADATA_PARAM; + /** * Description of the optional {@value #CREATE} parameter, which may be present in writable data store. * This parameter is not included in the descriptor created by {@link #build(ParameterBuilder)} default @@ -233,6 +224,7 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R final ParameterBuilder builder = new ParameterBuilder(); ENCODING = builder.addName("encoding").setDescription(Resources.formatInternational(Resources.Keys.DataStoreEncoding)).create(Charset.class, null); CREATE_PARAM = builder.addName( CREATE ).setDescription(Resources.formatInternational(Resources.Keys.DataStoreCreate )).create(Boolean.class, null); + METADATA_PARAM = builder.addName("metadata").setDescription(Resources.formatInternational(Resources.Keys.MetadataLocation )).create(Path.class, null); LOCATION_PARAM = builder.addName( LOCATION ).setDescription(Resources.formatInternational(Resources.Keys.DataStoreLocation)).setRequired(true).create(URI.class, null); } @@ -267,14 +259,14 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R /** * Invoked by {@link #getOpenParameters()} the first time that a parameter descriptor needs to be created. * When invoked, the parameter group name is set to a name derived from the {@link #getShortName()} value. - * The default implementation creates a group containing only {@link #LOCATION_PARAM}. + * The default implementation creates a group containing {@link #LOCATION_PARAM} and {@link #METADATA_PARAM}. * Subclasses can override if they need to create a group with more parameters. * * @param builder the builder to use for creating parameter descriptor. The group name is already set. * @return the parameters descriptor created from the given builder. */ protected ParameterDescriptorGroup build(final ParameterBuilder builder) { - return builder.createGroup(LOCATION_PARAM); + return builder.createGroup(LOCATION_PARAM, METADATA_PARAM); } /** @@ -390,10 +382,10 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R } /** - * Adds the filename (without extension) as the citation title if there are no titles, or as the identifier otherwise. + * Adds the filename (without extension) as the citation title if there is no title, or as the identifier otherwise. * This method should be invoked last, after {@code DataStore} implementation did its best effort for adding a title. - * The intent is actually to provide an identifier, but since the title is mandatory in ISO 19115 metadata, providing - * only an identifier without title would be invalid. + * The intend is actually to provide an identifier, but since the title is mandatory in ISO 19115 metadata, + * providing only an identifier without title would be invalid. * * @param builder where to add the title or identifier. */ @@ -403,4 +395,30 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R builder.addTitleOrIdentifier(filename, MetadataBuilder.Scope.ALL); } } + + /** + * If an auxiliary metadata file has been specified, merge that file to the given metadata. + * This step should be done only after the data store added its own metadata. + * Failure to load auxiliary metadata are only a warning. + * + * @param builder where to merge the metadata. + */ + protected final void mergeAuxiliaryMetadata(final MetadataBuilder builder) { + final Path path = getMetadataPath(); + if (path != null) try { + builder.mergeMetadata(XML.unmarshal(path), getLocale()); + } catch (JAXBException e) { + final Throwable cause = e.getCause(); + listeners.warning(cannotReadAuxiliaryFile("xml"), (cause instanceof IOException) ? (Exception) cause : e); + } + } + + /** + * {@return the error message for saying than auxiliary file cannot be read}. + * + * @param extension file extension of the auxiliary file, without leading dot. + */ + protected final String cannotReadAuxiliaryFile(final String extension) { + return Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotReadAuxiliaryFile_1, extension); + } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java index b1d5fede5d..02707d4dd7 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java @@ -642,6 +642,7 @@ final class Store extends URIDataStore implements FeatureSet { builder.addResourceScope(ScopeCode.FEATURE, null); builder.addExtent(envelope, listeners); builder.addFeatureType(featureType, -1); + mergeAuxiliaryMetadata(builder); addTitleOrIdentifier(builder); builder.setISOStandards(false); metadata = builder.buildAndFreeze(); diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java index 455864ab9a..56fcbbcf56 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java @@ -197,7 +197,7 @@ public final class StoreProvider extends URIDataStore.Provider { */ @Override protected ParameterDescriptorGroup build(final ParameterBuilder builder) { - return builder.createGroup(LOCATION_PARAM, ENCODING, FOLIATION); + return builder.createGroup(LOCATION_PARAM, METADATA_PARAM, ENCODING, FOLIATION); } /** diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java index 9b8f3cc9dd..9743c7dad7 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java @@ -46,7 +46,6 @@ import org.apache.sis.coverage.grid.j2d.ColorModelFactory; import org.apache.sis.coverage.grid.j2d.ImageUtilities; import org.apache.sis.coverage.grid.j2d.ObservableImage; import org.apache.sis.coverage.internal.RangeArgument; -import org.apache.sis.storage.internal.Resources; import org.apache.sis.util.CharSequences; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.internal.UnmodifiableArrayList; @@ -172,6 +171,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource builder.addNewBand(band); } } + mergeAuxiliaryMetadata(builder); addTitleOrIdentifier(builder); builder.setISOStandards(false); metadata = builder.buildAndFreeze(); @@ -426,7 +426,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource if (exception instanceof NoSuchFileException || exception instanceof FileNotFoundException) { level = Level.FINE; } - listeners.warning(level, Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, suffix), exception); + listeners.warning(level, cannotReadAuxiliaryFile(suffix), exception); } /** diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java index 1a5871868f..ddfa2321c0 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java @@ -357,8 +357,7 @@ final class RawRasterStore extends RasterStore { ByteOrder byteOrder = ByteOrder.nativeOrder(); final AuxiliaryContent header = readAuxiliaryFile(RawRasterStoreProvider.HDR); if (header == null) { - throw new DataStoreException(Resources.forLocale(getLocale()) - .getString(Resources.Keys.CanNotReadAuxiliaryFile_1, RawRasterStoreProvider.HDR)); + throw new DataStoreException(cannotReadAuxiliaryFile(RawRasterStoreProvider.HDR)); } for (CharSequence line : CharSequences.splitOnEOL(header)) { final int length = line.length(); diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java index 6da2247bb3..5293f2c541 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java @@ -47,7 +47,6 @@ import org.apache.sis.storage.DataStoreClosedException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.ReadOnlyStorageException; import org.apache.sis.storage.UnsupportedStorageException; -import org.apache.sis.storage.internal.Resources; import org.apache.sis.storage.base.PRJDataStore; import org.apache.sis.storage.base.MetadataBuilder; import org.apache.sis.referencing.util.j2d.AffineTransform2D; @@ -357,7 +356,7 @@ loop: for (int convention=0;; convention++) { } } if (warning != null) { - listeners.warning(resources().getString(Resources.Keys.CanNotReadAuxiliaryFile_1, preferred), warning); + listeners.warning(cannotReadAuxiliaryFile(preferred), warning); } return null; } @@ -373,7 +372,7 @@ loop: for (int convention=0;; convention++) { private AffineTransform2D readWorldFile(final String wld) throws IOException, DataStoreException { final AuxiliaryContent content = readAuxiliaryFile(wld); if (content == null) { - listeners.warning(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, wld)); + listeners.warning(cannotReadAuxiliaryFile(wld)); return null; } final String filename = content.getFilename(); @@ -406,18 +405,11 @@ loop: for (int convention=0;; convention++) { return new AffineTransform2D(elements); } - /** - * Returns the localized resources for producing warnings or error messages. - */ - final Resources resources() { - return Resources.forLocale(listeners.getLocale()); - } - /** * Returns the localized resources for producing error messages. */ private Errors errors() { - return Errors.getResources(listeners.getLocale()); + return Errors.getResources(getLocale()); } /** @@ -539,6 +531,7 @@ loop: for (int convention=0;; convention++) { if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) { builder.addExtent(gridGeometry.getEnvelope(), listeners); } + mergeAuxiliaryMetadata(builder); addTitleOrIdentifier(builder); builder.setISOStandards(false); metadata = builder.buildAndFreeze(); diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java index fb945643d1..ec40e5651b 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableStore.java @@ -350,6 +350,13 @@ writeCoeffs: for (int i=0;; i++) { Resources.Keys.CanNotRemoveResource_2, getDisplayName(), label(resource)), cause); } + /** + * Returns the localized resources for producing warnings or error messages. + */ + final Resources resources() { + return Resources.forLocale(getLocale()); + } + /** * Returns a label for the given resource in error messages. */ diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java index 63506fd42f..b556fd4937 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java @@ -307,6 +307,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short MarkNotSupported_1 = 62; + /** + * Relative path to metadata. + */ + public static final short MetadataLocation = 81; + /** * Resource “{0}” does not have an identifier. */ diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties index f76fb8118f..6356fb4950 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties @@ -69,6 +69,7 @@ InvalidExpression_2 = Invalid or unsupported \u201c{1}\u201d expre ExceptionInListener_1 = Exception occurred in a listener for events of type \u2018{0}\u2019. LoadedGridCoverage_6 = Loaded grid coverage between {1} \u2013 {2} and {3} \u2013 {4} from file \u201c{0}\u201d in {5} seconds. MarkNotSupported_1 = Marks are not supported on \u201c{0}\u201d stream. +MetadataLocation = Relative path to metadata. MissingResourceIdentifier_1 = Resource \u201c{0}\u201d does not have an identifier. MissingSchemeInURI_1 = Missing scheme in \u201c{0}\u201d URI. NoCommonFeatureType = No feature type is common to all the features to aggregate. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties index b950c2eb63..6b742719e3 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties @@ -74,6 +74,7 @@ InconsistentNameComponents_2 = Les \u00e9l\u00e9ments qui composent le nom ExceptionInListener_1 = Une exception est survenue dans un capteur d\u2019\u00e9v\u00e9nements de type \u2018{0}\u2019. LoadedGridCoverage_6 = Lecture d\u2019une couverture de donn\u00e9es entre {1} \u2013 {2} et {3} \u2013 {4} \u00e0 partir du fichier \u00ab\u202f{0}\u202f\u00bb en {5} secondes. MarkNotSupported_1 = Les marques ne sont pas support\u00e9es sur le flux \u00ab\u202f{0}\u202f\u00bb. +MetadataLocation = Chemin relatif des m\u00e9ta-donn\u00e9es. MissingResourceIdentifier_1 = La ressource \u00ab\u202f{0}\u202f\u00bb n\u2019a pas d\u2019identifiant. MissingSchemeInURI_1 = Il manque le sch\u00e9ma dans l\u2019URI \u00ab\u202f{0}\u202f\u00bb. NoCommonFeatureType = Il n\u2019y a pas de type commun \u00e0 toutes les entit\u00e9s \u00e0 agr\u00e9ger. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java index aa5a54c3dd..1e4a7099a0 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java @@ -181,7 +181,9 @@ final class Store extends URIDataStore { } if (count == 1) { // Set the citation title only if non-ambiguous. builder.addTitle(name); + mergeAuxiliaryMetadata(builder); } else { + mergeAuxiliaryMetadata(builder); addTitleOrIdentifier(builder); } metadata = builder.buildAndFreeze(); diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java index 6e15afeb29..a432ff8673 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java @@ -207,6 +207,7 @@ final class Store extends URIDataStore implements Filter { final MetadataBuilder builder = new MetadataBuilder(); builder.addReferenceSystem((ReferenceSystem) object); builder.addTitle(getDisplayName()); + mergeAuxiliaryMetadata(builder); metadata = builder.buildAndFreeze(); } } diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/OptionKey.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/OptionKey.java index 294a417bd2..0fc616d6f8 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/OptionKey.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/OptionKey.java @@ -27,6 +27,7 @@ import java.nio.charset.Charset; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; import static java.util.logging.Logger.getLogger; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.logging.Logging; import org.apache.sis.system.Modules; @@ -62,7 +63,7 @@ import org.apache.sis.system.Modules; * } * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.5 * * @param <T> the type of option values. * @@ -181,6 +182,17 @@ public class OptionKey<T> implements Serializable { */ public static final OptionKey<ByteBuffer> BYTE_BUFFER = new OptionKey<>("BYTE_BUFFER", ByteBuffer.class); + /** + * The coordinate reference system (CRS) of data to use if not explicitly defined. + * This option can be used when the file to read does not describe itself the data CRS. + * For example, this option can be used when reading ASCII Grid without CRS information, + * but is ignored if the ASCII Grid file is accompanied by a {@code *.prj} file giving the CRS. + * + * @since 1.5 + */ + public static final OptionKey<CoordinateReferenceSystem> DEFAULT_CRS = + new OptionKey<>("DEFAULT_CRS", CoordinateReferenceSystem.class); + /** * The library to use for creating geometric objects at reading time. * Some libraries are the Java Topology Suite (JTS), ESRI geometry API and Java2D. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/package-info.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/package-info.java index 01f85a6d86..37c25bb411 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/package-info.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/package-info.java @@ -23,7 +23,7 @@ * is created. * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.5 * @since 0.3 */ package org.apache.sis.setup; diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java index f6af6788a1..328878e664 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ArraysExt.java @@ -1237,15 +1237,60 @@ public final class ArraysExt extends Static { return copy; } + /** + * Removes all null elements in the given array. For each null element found in the array at index <var>i</var>, + * all elements at indices <var>i</var>+1, <var>i</var>+2, <var>i</var>+3, <i>etc.</i> are moved to indices + * <var>i</var>, <var>i</var>+1, <var>i</var>+2, <i>etc.</i> + * This method returns the new array length, which is {@code array.length} minus the number of null elements. + * The array content at indices equal or greater than the new length is undetermined. + * + * <p>Callers can obtain an array of appropriate length using the following idiom. + * Note that this idiom will create a new array only if necessary:</p> + * + * {@snippet lang="java" : + * T[] array = ...; + * array = resize(array, removeNulls(array)); + * } + * + * @param array array from which to remove null elements, or {@code null}. + * @return the number of remaining elements in the given array, or 0 if the given {@code array} was null. + * + * @since 1.5 + */ + public static int removeNulls(final Object[] array) { + if (array == null) { + return 0; + } + int i; + for (i=0; ; i++) { + if (i >= array.length) return i; // Return if all values are non-null. + if (array[i] == null) break; // Stop without incrementing `i`. + } + Object value; + int count = i; + do if (++i >= array.length) return count; // Common case where all remaining values are null. + while ((value = array[i]) == null); + + // Start copying values only on the portion of the array where it is needed. + array[count++] = value; + while (++i < array.length) { + value = array[i]; + if (value != null) { + array[count++] = value; + } + } + return count; + } + /** * Removes the duplicated elements in the given array. This method should be invoked only for small arrays, * typically less than 10 distinct elements. For larger arrays, use {@link java.util.LinkedHashSet} instead. * - * <p>This method compares all pair of elements using the {@link Objects#equals(Object, Object)} - * method - so null elements are allowed. If duplicated values are found, then only the first - * occurrence is retained; the second occurrence is removed in-place. After all elements have - * been compared, this method returns the number of remaining elements in the array. The free - * space at the end of the array is padded with {@code null} values.</p> + * <p>This method compares all pairs of elements using the {@link Objects#equals(Object, Object)} method - + * so null elements are allowed. If duplicated values are found, + * then only the first occurrence is retained and the second occurrence is removed in-place. + * After all elements have been compared, this method returns the number of remaining elements in the array. + * The free space at the end of the array is padded with {@code null} values.</p> * * <p>Callers can obtain an array of appropriate length using the following idiom. * Note that this idiom will create a new array only if necessary:</p> @@ -1255,11 +1300,6 @@ public final class ArraysExt extends Static { * array = resize(array, removeDuplicated(array)); * } * - * <div class="note"><b>API note:</b> - * This method return type is not an array in order to make obvious that the given array will be modified in-place. - * This behavior is different than the behavior of many other methods in this class, which do not modify the given - * source array.</div> - * * @param array array from which to remove duplicated elements, or {@code null}. * @return the number of remaining elements in the given array, or 0 if the given {@code array} was null. */ @@ -1267,7 +1307,28 @@ public final class ArraysExt extends Static { if (array == null) { return 0; } - int length = array.length; + return removeDuplicated(array, array.length); + } + + /** + * Removes the duplicated elements in the first elements of the given array. + * This method performs the same work than {@link #removeDuplicated(Object[])}, + * but taking in account only the first {@code length} elements. The latter argument + * is convenient for chaining this method after {@link #removeNulls(Object[])} as below: + * + * {@snippet lang="java" : + * T[] array = ...; + * array = resize(array, removeDuplicated(array, removeNulls(array))); + * } + * + * @param array array from which to remove duplicated elements, or {@code null}. + * @param length number of elements to examine at the beginning of the array. + * @return the number of remaining elements in the given array, or 0 if the given {@code array} was null. + * @throws ArrayIndexOutOfBoundsException if {@code length} is negative or greater than {@code array.length}. + * + * @since 1.5 + */ + public static int removeDuplicated(final Object[] array, int length) { for (int i=length; --i>=0;) { final Object value = array[i]; for (int j=i; --j>=0;) { diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/CollectionsExt.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/CollectionsExt.java index d24b297d2e..e43ae7e5d2 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/CollectionsExt.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/CollectionsExt.java @@ -139,6 +139,22 @@ public final class CollectionsExt extends Static { return null; } + /** + * Returns the last element of the given iterable if it is a list, or an arbitrary element otherwise. + * + * @todo Check for sequenced collection in JDK21. + * + * @param <T> the type of elements contained in the iterable. + * @param collection the iterable from which to get the last element, or {@code null}. + * @return the last element, or {@code null} if the given iterable is null or empty. + */ + public static <T> T last(final Collection<T> collection) { + if (collection instanceof List<?> && !collection.isEmpty()) { + return ((List<T>) collection).get(collection.size() - 1); + } + return null; + } + /** * If the given iterable contains exactly one non-null element, returns that element. * Otherwise returns {@code null}. diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/ArraysExtTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/ArraysExtTest.java index 8dd68574be..cc70fad354 100644 --- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/ArraysExtTest.java +++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/ArraysExtTest.java @@ -18,7 +18,7 @@ package org.apache.sis.util; // Test dependencies import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import org.apache.sis.test.TestCase; @@ -75,6 +75,16 @@ public final class ArraysExtTest extends TestCase { ArraysExt.resize(array, ArraysExt.removeDuplicated(array))); } + /** + * Tests {@link ArraysExt#removeNulls(Object[])}. + */ + @Test + public void testRemoveNulls() { + final Integer[] array = new Integer[] {2, 8, null, 4, null, 3, 1, null, null}; + assertArrayEquals(new Integer[] {2, 8, 4, 3, 1}, + ArraysExt.resize(array, ArraysExt.removeNulls(array))); + } + /** * Tests {@link ArraysExt#reverse(int[])}. * The test uses an array of even length, then an array of odd length. @@ -96,11 +106,11 @@ public final class ArraysExtTest extends TestCase { @Test public void testRange() { int[] sequence = ArraysExt.range(-1, 3); - assertArrayEquals("range", new int[] {-1, 0, 1, 2}, sequence); - assertTrue ("isRange", ArraysExt.isRange(-1, sequence)); - assertFalse("isRange", ArraysExt.isRange(-2, sequence)); - assertTrue ("isRange", ArraysExt.isRange(1, new int[] {1, 2, 3, 4})); - assertFalse("isRange", ArraysExt.isRange(1, new int[] {1, 2, 4})); + assertArrayEquals(new int[] {-1, 0, 1, 2}, sequence); + assertTrue (ArraysExt.isRange(-1, sequence)); + assertFalse(ArraysExt.isRange(-2, sequence)); + assertTrue (ArraysExt.isRange(1, new int[] {1, 2, 3, 4})); + assertFalse(ArraysExt.isRange(1, new int[] {1, 2, 4})); } /** @@ -253,7 +263,7 @@ public final class ArraysExtTest extends TestCase { public void testSwapFloat() { final float[] array = new float[] {4, 8, 12, 15, 18}; ArraysExt.swap(array, 1, 3); - assertArrayEquals(new float[] {4, 15, 12, 8, 18}, array, 0f); + assertArrayEquals(new float[] {4, 15, 12, 8, 18}, array); } /** @@ -314,7 +324,7 @@ public final class ArraysExtTest extends TestCase { double[] array = {2, 0.5, 0.25, Double.NaN, Double.POSITIVE_INFINITY}; float[] result = ArraysExt.copyAsFloatsIfLossless(array); assertNotNull(result); - assertArrayEquals(new float[] {2f, 0.5f, 0.25f, Float.NaN, Float.POSITIVE_INFINITY}, result, 0f); + assertArrayEquals(new float[] {2f, 0.5f, 0.25f, Float.NaN, Float.POSITIVE_INFINITY}, result); array[3] = 0.3333333333333; assertNull(ArraysExt.copyAsFloatsIfLossless(array)); }