001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2012-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.test.coverage.image;
019
020import javax.imageio.ImageReader;
021import javax.imageio.ImageWriter;
022import javax.imageio.spi.ImageReaderSpi;
023import javax.imageio.spi.ImageWriterSpi;
024import javax.imageio.spi.ImageReaderWriterSpi;
025import javax.imageio.metadata.IIOMetadataFormat;
026import javax.imageio.metadata.IIOMetadataFormatImpl;
027
028import org.opengis.test.Validator;
029import org.opengis.test.ValidatorContainer;
030import org.opentest4j.AssertionFailedError;
031
032import static org.junit.jupiter.api.Assertions.*;
033
034
035/**
036 * Validators for implementations of {@link java.awt.image} or {@link javax.imageio} services.
037 *
038 * @author  Martin Desruisseaux (Geomatys)
039 * @version 3.1
040 * @since   3.1
041 */
042public class ImageValidator extends Validator {
043    /**
044     * Creates a new validator instance.
045     *
046     * @param container  the set of validators to use for validating other kinds of objects
047     *                   (see {@linkplain #container field javadoc}).
048     */
049    public ImageValidator(final ValidatorContainer container) {
050        super(container, "org.opengis.test.coverage.image");
051    }
052
053    /**
054     * Validates the given provider of image readers.
055     * First, this method verifies that mandatory elements are non-null, arrays are non-empty
056     * (Image I/O specification requires them to be {@code null} rather than empty), and class
057     * names are valid. Next, this method invokes {@link #validate(IIOMetadataFormat)} for each
058     * metadata format (which can be null).
059     *
060     * @param provider  the provider to validate, or {@code null} if none.
061     */
062    public void validate(final ImageReaderSpi provider) {
063        if (provider != null) {
064            validateProvider(provider, ImageReader.class);
065            final Class<?>[] inputTypes = provider.getInputTypes();
066            mandatory(inputTypes, "ImageReaderSpi: shall have an inputTypes array.");
067            validateArray("inputTypes", inputTypes);
068            final String[] imageWriterSpiNames = provider.getImageWriterSpiNames();
069            validateArray("imageWriterSpiNames", imageWriterSpiNames);
070            if (imageWriterSpiNames != null) {
071                final ClassLoader loader = provider.getClass().getClassLoader();
072                for (int i=0; i<imageWriterSpiNames.length; i++) {
073                    final String field = "imageWriterSpiNames[" + i + ']';
074                    final String className = imageWriterSpiNames[i];
075                    assertNotNull(className, () -> field + " cannot be null.");
076                    validateClass(field, ImageWriterSpi.class, loader, className);
077                }
078            }
079        }
080    }
081
082    /**
083     * Validates the given provider of image writers.
084     * First, this method verifies that mandatory elements are non-null, arrays are non-empty
085     * (Image I/O specification requires them to be {@code null} rather than empty), and class
086     * names are valid. Next, this method invokes {@link #validate(IIOMetadataFormat)} for each
087     * metadata format (which can be null).
088     *
089     * @param provider  the provider to validate, or {@code null} if none.
090     */
091    public void validate(final ImageWriterSpi provider) {
092        if (provider != null) {
093            validateProvider(provider, ImageWriter.class);
094            final Class<?>[] outputTypes = provider.getOutputTypes();
095            mandatory(outputTypes, "ImageWriterSpi: shall have an outputTypes array.");
096            validateArray("outputTypes", outputTypes);
097            final String[] imageReaderSpiNames = provider.getImageReaderSpiNames();
098            validateArray("imageReaderSpiNames", imageReaderSpiNames);
099            if (imageReaderSpiNames != null) {
100                final ClassLoader loader = provider.getClass().getClassLoader();
101                for (int i=0; i<imageReaderSpiNames.length; i++) {
102                    final String field = "imageReaderSpiNames[" + i + ']';
103                    final String className = imageReaderSpiNames[i];
104                    assertNotNull(className, () -> field + " cannot be null.");
105                    validateClass(field, ImageReaderSpi.class, loader, className);
106                }
107            }
108        }
109    }
110
111    /**
112     * Validates the given image reader or image writer provider.
113     *
114     * @param spi  the provider to validate, or {@code null} if none.
115     * @param pluginType  expected provider base class.
116     */
117    private void validateProvider(final ImageReaderWriterSpi spi, final Class<?> pluginType) {
118        mandatory(spi.getVendorName(), "ImageReaderWriterSpi: shall have a vendorName string.");
119        mandatory(spi.getVersion(),    "ImageReaderWriterSpi: shall have a version string.");
120        final String[] formatNames = spi.getFormatNames();
121        mandatory(formatNames, "ImageReaderWriterSpi: shall have a formatNames array.");
122        validateArray("formatNames",  formatNames);
123        validateArray("fileSuffixes", spi.getFileSuffixes());
124        validateArray("MIMETypes",    spi.getMIMETypes());
125        validateClass("pluginClassName", pluginType, spi.getClass().getClassLoader(), spi.getPluginClassName());
126        validateMetadataFormatName("Stream", spi.getNativeStreamMetadataFormatName(), spi.getExtraStreamMetadataFormatNames());
127        validateMetadataFormatName("Image",  spi.getNativeImageMetadataFormatName(),  spi.getExtraImageMetadataFormatNames());
128        /*
129         * Ensures that a IIOMetadataFormat can be instantiated for each declared format name.
130         * Then, invokes validate(IIOMetadataFormat) for each format. Note that the format are
131         * allowed to be null according Image I/O specification.
132         */
133        if (spi.isStandardStreamMetadataFormatSupported()) {
134            assertSame(IIOMetadataFormatImpl.getStandardFormatInstance(),
135                       spi.getStreamMetadataFormat(IIOMetadataFormatImpl.standardMetadataFormatName),
136                       "Expected the standard metadata format instance.");
137        }
138        if (spi.isStandardImageMetadataFormatSupported()) {
139            assertSame(IIOMetadataFormatImpl.getStandardFormatInstance(),
140                       spi.getImageMetadataFormat(IIOMetadataFormatImpl.standardMetadataFormatName),
141                       "Expected the standard metadata format instance.");
142        }
143        String formatName = spi.getNativeStreamMetadataFormatName();
144        if (formatName != null) {
145            validate(spi.getStreamMetadataFormat(formatName));
146        }
147        formatName = spi.getNativeImageMetadataFormatName();
148        if (formatName != null) {
149            validate(spi.getImageMetadataFormat(formatName));
150        }
151        String[] names = spi.getExtraStreamMetadataFormatNames();
152        if (names != null) {
153            for (final String name : names) {
154                validate(spi.getStreamMetadataFormat(name));
155            }
156        }
157        names = spi.getExtraImageMetadataFormatNames();
158        if (names != null) {
159            for (final String name : names) {
160                validate(spi.getImageMetadataFormat(name));
161            }
162        }
163    }
164
165    /**
166     * Validates the image or stream metadata format names.
167     * This method ensures that there is no duplicated values.
168     *
169     * @param type  the type of metadata to validate: "Stream" or "Image".
170     * @param nativeMetadataFormatName  the name of the native metadata format.
171     * @param extraMetadataFormatNames  the name of the extra metadata format.
172     */
173    private static void validateMetadataFormatName(final String type, String nativeMetadataFormatName,
174            final String[] extraMetadataFormatNames)
175    {
176        if (nativeMetadataFormatName != null) {
177            nativeMetadataFormatName = nativeMetadataFormatName.trim();
178            assertFalse(IIOMetadataFormatImpl.standardMetadataFormatName.equalsIgnoreCase(nativeMetadataFormatName),
179                    () -> "The native" + type + "MetadataFormatName value cannot be equal to \""
180                            + IIOMetadataFormatImpl.standardMetadataFormatName + "\".");
181        }
182        if (extraMetadataFormatNames != null) {
183            final String field = "extra" + type + "MetadataFormatNames";
184            validateArray(field, extraMetadataFormatNames);
185            for (int i=0; i<extraMetadataFormatNames.length; i++) {
186                final String formatName = extraMetadataFormatNames[i].trim();
187                if (IIOMetadataFormatImpl.standardMetadataFormatName.equalsIgnoreCase(formatName)) {
188                    fail("The " + field + '[' + i + "] value cannot be equal to \"" +
189                            IIOMetadataFormatImpl.standardMetadataFormatName + "\".");
190                }
191                if (nativeMetadataFormatName != null && nativeMetadataFormatName.equalsIgnoreCase(formatName)) {
192                    fail("The " + field + '[' + i + "] value cannot be equal to \"" + nativeMetadataFormatName
193                            + "\" because the latter is already declared as the native format name.");
194                }
195            }
196        }
197    }
198
199    /**
200     * Ensures that the given array complies with the conditions of Java Image I/O.
201     * The array can be either null or non-empty; empty arrays are illegal.
202     * Then ensures that there is no duplicated value.
203     *
204     * @param field  the field name, used in case of errors.
205     * @param array  the array to validate, or {@code null} if none.
206     */
207    private static void validateArray(final String field, final Object[] array) {
208        if (array != null) {
209            assertNotEquals(array.length, 0, () -> "The " + field + " array shall be either null or non-empty.");
210            for (int i=0; i<array.length; i++) {
211                final Object element = array[i];
212                if (element == null) {
213                    fail(field + '[' + i + "] is null.");
214                }
215                for (int j=i; ++i<array.length;) {
216                    if (element.equals(array[i])) {
217                        fail(field + '[' + i + "] and [" + j + "] duplicate the \"" + element + "\" value.");
218                    }
219                }
220            }
221        }
222    }
223
224    /**
225     * Ensure that given class exists and is an instance of the given type.
226     *
227     * @param field         the name of the tested field.
228     * @param expectedType  the expected base class.
229     * @param loader        the loader to use for loading the class.
230     * @param classname     the name of the class to test.
231     */
232    private void validateClass(final String field, final Class<?> expectedType,
233            final ClassLoader loader, final String classname)
234    {
235        mandatory(classname, "ImageReaderWriterSpi: shall have a " + field + " string.");
236        if (classname != null) try {
237            final Class<?> actual = Class.forName(classname, false, loader);
238            assertTrue(expectedType.isAssignableFrom(actual),
239                    () -> actual.getCanonicalName() + " is not an instance of " + expectedType.getSimpleName() + '.');
240        } catch (ClassNotFoundException e) {
241            throw new AssertionFailedError("Class \"" + classname + "\" declared in " + field + " was not found.", e);
242        }
243    }
244
245    /**
246     * Validates the given metadata format.
247     *
248     * @param format  the metadata format to validate, or {@code null} if none.
249     */
250    public void validate(final IIOMetadataFormat format) {
251        // Not yet implemented.
252    }
253}