1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
package Torello.HTML.Tools.Images;

import Torello.Java.StringParse;
import Torello.Java.StrPrint;

import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.net.URL;
import java.util.Objects;

/**
 * Simple Image Data-Class that is instantiated by the {@link ImageScraper}, and passed to any of
 * the {@code FunctionalInterface}'s or Lambda-Targets that are non-null / available in the user's
 * {@link Request} object instance.
 * 
 * <EMBED CLASS='external-html' DATA-FILE-ID=IMAGE_INFO_EXAMPLE>
 */
@Torello.JavaDoc.JDHeaderBackgroundImg(EmbedTagFileID="IMAGE_SCRAPER_CLASS")
public class ImageInfo implements Cloneable, Serializable
{
    /** <EMBED CLASS='external-html' DATA-FILE-ID=SVUID> */
    public static final long serialVersionUID = 1;


    // ********************************************************************************************
    // ********************************************************************************************
    // Public Fields
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * The {@code URL} that was used to download the image.  Note that anytime this instance of
     * {@code ImageInfo} represents an image that was located on a web-page encoded using a 
     * Base-64 {@code String}, then this field will be null, and the B-64 Fields will contain the
     * relevant image data (not this {@code URL}).
     */
    public final URL url;

    /**
     * If the image whose details are contained by this class-instance are from an image that
     * was encoded using the {@code String}-literal Base-64 Encoding Algorithm, then this boolean
     * flag will contain {@code TRUE}.
     * 
     * <BR /><BR />An HTML {@code <IMG SRC=...>} Tag is the only way to enter a Base-64 Image into
     * the Image-Scraper class.
     */
    public final boolean isB64EncodedImage;

    /**
     * A web-page has the ability to inline (smaller) images directly into an HTML
     * {@code <IMG SRC=...>} tag by encoding the picture into a Base-64 {@code String}.  The
     * {@code String} is saved inside the {@code SRC}-Attribute of the {@code <IMG>} tag, and
     * contains two separate sub-strings.
     * 
     * <BR /><BR />This is the first sub-string, and it just identifies / lists the format 
     * ({@code .jpg, .gif, .png} etc...) in which the picture was saved before translating it into
     * Base-64 Encoded Text.
     * 
     * <BR /><BR />If the picture represented by this instance of {@code ImageInfo} was downloaded
     * from a {@code URL}, then this field will be null, and the {@link #url} field will contain
     * the Image {@code URL}.
     * 
     * @see #isB64EncodedImage
     * @see #b64EncodedImage
     */
    public final String imageFormatStr;

    /**
     * A web-page has the ability to inline (smaller) images directly into an HTML
     * {@code <IMG SRC=...>} tag by encoding the picture into a Base-64 {@code String}.  The
     * {@code String} is saved inside the {@code SRC}-Attribute of the {@code <IMG>} tag, and
     * contains two separate sub-strings.
     * 
     * <BR /><BR />This is the second sub-string, and it is the text after translating the picture
     * into Base-64 Encoded Text.
     * 
     * <BR /><BR />If the picture represented by this instance of {@code ImageInfo} was downloaded
     * from a {@code URL}, then this field will be null, and the {@link #url} field will contain
     * the Image {@code URL}.
     * 
     * @see #isB64EncodedImage
     * @see #imageFormatStr
     */
    public final String b64EncodedImage;

    /** The image, as prepared for saving to disk. */
    public final byte[] imgByteArr;

    /** The image, as an instance of {@code java.awt.image.BufferedImage} */
    public final BufferedImage bufferedImage;

    /**
     * The value returned by {@code bufferedImage.getWidth()} - <I>the downloaded image's
     * width</I>.
     */
    public final int width;

    /**
     * The value returned by {@code bufferedImage.getHeight()} - <I>the downloaded image's
     * height</I>.
     */
    public final int height;

    /**
     * This shall help identify whether the image-in-question is a {@code GIF, JPG, PNG, etc...}.
     * The field {@code 'guessedImageFormat'} shall simply contain the image-type based on the
     * extension found in the {@code URL's} file-name.
     * 
     * <BR /><BR />There are web-pages &amp; web-sites that do not provide a file-name extension
     * for the images they use on their page(s).  In such cases, the downloader will eventually
     * attempt to 'guess' the format of an image that has been downloaded.  In these cases, this
     * parameter will be passed null, and the parameter {@code actualImageFormat} will contain the
     * format that was actually used to successfully save the data to disk.
     */
    public final IF guessedExtension;

    /**
     * If the image has been properly converted, and is ready to be written to disk, this parameter
     * will contain the {@link IF} / image-format that was used to successfully save the image.
     * 
     * <BR /><BR />Note that often (but not always), this extension / {@link IF} instance will be
     * identical to the parameter {@code 'guessedImageFormat'}.  There will be cases, as mentioned
     * above, when {@code 'guessImageFormat'} is null.  Furthermore, there may be (very rare)
     * situations when an image-format for a particular {@code URL} was incorrect, and was saved
     * properly using a different format &amp; extension.
     */
    public final IF actualExtension;

    /**
     * Identifies the <B STYLE='color: red;'>count</B> in the {@code Iterator's} retrieval.  Since
     * this {@code int} is used as an array-index pointer, it is initialized to {@code '0'} (zero).
     * Specifically, if this method were called upon completion of three iterations of
     * Image-{@code URL} retrieval, this counter would contain the integer {@code '2'} (two).
     */
    public final int iteratorCounter;

    /**
     * This identifies how many images have successfully downloaded, not the number of images for
     * which a "download attempt" occurred.  Since this {@code int} is used as an array-index
     * pointer, it is initialized to {@code '0'} (zero).  If on the third iteration of the
     * source-{@code Iterator}, an {@code IOException} occurred between the Java-Virtual-Machine
     * and the internet, the following invocation of this method would have {@code successCounter}
     * as {@code '2'}, but the {@code iteratorCounter} would be {@code '3'}.
     */
    public final int successCounter;

    // This data that is saved in this field isn't saved at the time of construction, and therefore
    // this field cannot be 'final'.  If this field cannot be 'final', it cannot be public, so I 
    // guess there just has to be a getter-method for this one.

    private String fileName = null;


    // ********************************************************************************************
    // ********************************************************************************************
    // Package-Private Constructor
    // ********************************************************************************************
    // ********************************************************************************************


    // Used in the "Main Loop Body", only Once
    ImageInfo(
            // Image-URL (very common)
            URL url,

            // Base-64 Image Stuff (rare, but not impossible)
            boolean isB64EncodedImage,
            String[] b64ImageData,

            // The actual downloaded and converted images, themselves
            BufferedImage bufferedImage,
            byte[] imgByteArr,

            // URL-Aquired Extension & Ultimately-Decided-Upon Extension
            IF guessedExtension,
            IF actualExtension,

            // class 'Results' Array-Counters (index-pointers)
            int iteratorCounter,
            int successCounter
        )
    {
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // This is just "Paranoia" - but fortunately, it isn't very "expensive" paranoia
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        if ((url == null) && (! isB64EncodedImage)) throw new IllegalArgumentException
            ("(url == null) && (! isB64EncodedImage)");

        if ((url != null) && isB64EncodedImage) throw new IllegalArgumentException
            ("(url != null) && isB64EncodedImage");

        if (isB64EncodedImage && (b64ImageData == null)) throw new IllegalArgumentException
            ("isB64EncodedImage && (b64ImageData == null)");

        if ((! isB64EncodedImage) && (b64ImageData != null)) throw new IllegalArgumentException
            ("(! isB64EncodedImage) && (b64ImageData != null)");


        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
        // Initialize all 'final' fields!  The only one left out is 'fileName' - it is done later
        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

        // Image-URL (very common)
        this.url = url;

        // Base-64 Image Stuff (rare, but not impossible)
        this.isB64EncodedImage  = isB64EncodedImage;
        this.imageFormatStr     = isB64EncodedImage ? b64ImageData[0] : null;
        this.b64EncodedImage    = isB64EncodedImage ? b64ImageData[1] : null;

        // The actual downloaded and converted images, themselves
        this.imgByteArr = imgByteArr;
        this.bufferedImage = bufferedImage;

        // Note sure how necessary / important this is, but maybe...
        this.width = bufferedImage.getWidth();
        this.height = bufferedImage.getHeight();

        // URL-Aquired Extension & Ultimately-Decided-Upon Extension
        this.guessedExtension = guessedExtension;
        this.actualExtension = actualExtension;

        // class 'Results' Array-Counters (index-pointers)
        this.iteratorCounter = iteratorCounter;
        this.successCounter = successCounter;
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // Lone Accessor Method
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * A "getter" (Accessor-Method) for the {@code private}-Field named {@code fileName}.  This
     * field is kept {@code private} because it cannot be declared {@code final} - since it is
     * initialized several steps after construction.
     * 
     * <BR /><BR />If a user Lambda-Expression / Functional-Interface has recevied {@code 'this'}
     * instance of {@code ImageInfo}, and needs access to the ultimately-decided-upon image
     * file-name, then this field may be retrieved using this 'Getter' Method.
     * 
     * <BR /><BR /><B CLASS=JDDescLabel>Unitialized at Construction:</B>
     * <BR />Note that this method will return null if a user attempts to retrieve the file-name
     * before it has been decided upon and set in this class.  This is actually the whole reason
     * that it cannot be declared final, and therefore cannot be declared public (and requires this
     * Getter-Method).
     */
    public String fileName() { return fileName; }

    // Package-Private, this is set inside class "ImageScraper"
    void setFileName(String fileName) { this.fileName = fileName; }
    

    // ********************************************************************************************
    // ********************************************************************************************
    // interface java.lang.Cloneable
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * Generates a <B STYLE='color: red;'>Shallow Copy</B> of {@code 'this'} instance.  This means
     * that the images themselves are not copied - rather only the references to the images are
     * copied into the clone.
     * 
     * <BR /><BR />The non-reference, non-instance (primitive-type) fields are all "just copied
     * like normal" :)
     * 
     * @return A duplicate instance of this class.
     */
    public ImageInfo clone()
    { return new ImageInfo(this); }

    // Private Constructor, used only for the 'clone()' method
    private ImageInfo(ImageInfo r)
    {
        // Image-URL (very common)
        this.url = r.url;

        // Base-64 Image Stuff (rare, but not impossible)
        this.isB64EncodedImage  = r.isB64EncodedImage;
        this.imageFormatStr     = r.imageFormatStr;
        this.b64EncodedImage    = r.b64EncodedImage;

        // The actual downloaded and converted images, themselves
        this.imgByteArr = r.imgByteArr;
        this.bufferedImage = r.bufferedImage;

        // Image-Width, Image-Height
        this.width = r.width;
        this.height = r.height;

        // URL-Aquired Extension & Ultimately-Decided-Upon Extension
        this.guessedExtension = r.guessedExtension;
        this.actualExtension = r.actualExtension;

        // class 'Results' Array-Counters (index-pointers)
        this.iteratorCounter = r.iteratorCounter;
        this.successCounter = r.successCounter;

        // This is the lone / only 'non-final' field
        this.fileName = r.fileName;
    }


    // ********************************************************************************************
    // ********************************************************************************************
    // java.lang.Object
    // ********************************************************************************************
    // ********************************************************************************************


    /**
     * Converts this class into a simple, readable {@code String}
     * @return A {@code java.lang.String} representation of {@code 'this'} instance.
     */
    public String toString()
    {
        return
            ((this.url != null)
                ? ("URL: " + StrPrint.abbrev(this.url.toString(), 40, true, " ... ", 80))
                : "") +

            (this.isB64EncodedImage
                ? ("B64-Encoded Format: " + this.imageFormatStr + ", IMG: " +
                    StrPrint.abbrev(this.b64EncodedImage, 30, true, " ... ", 60))
                : "") +

            '\n' +
            
            "Byte-Array.length: "   + StringParse.commas(this.imgByteArr.length) + '\n' +

            "W: "                   + StringParse.commas(this.width) + ", " +
            "H: "                   + StringParse.commas(this.height) + ", " +
            "File-Extension: "      + Objects.toString(this.actualExtension) + ", " +
            "URL-Extension: "       + Objects.toString(this.guessedExtension) + '\n' +

            "Iterator-Count: "      + this.iteratorCounter + ", " +
            "Downloaded-Count: "    + this.successCounter + '\n' +

            "Saving File-Name: "    + this.fileName + '\n';
    }

    /**
     * Checks whether {@code 'this'} instance is equal to {@code 'other'}.
     *
     * @param other Any Java Object, but only an instance {@code ImageInfo} (or a class that is
     * assignable to it) could possible generate a {@code TRUE}-return value.
     *
     * @return {@code TRUE} If and only if {@code 'other'} is an instance of {@code ImageInfo}, and
     * if the contents of that are instance are identical to the contents of {@code 'this'}
     * instance.
     */
    public boolean equals(Object other)
    {
        if (other == null) return false;

        if (! ImageInfo.class.isAssignableFrom(other.getClass())) return false;

        ImageInfo ii = (ImageInfo) other;

        // Note that using 'Objects.equals(...)' and 'Objects.deepEquals(...)' primarily prevents
        // a NullPointerException from being thrown if the left side of an '.equals(...)' were to
        // be null.  It's really nothing more than that.  (A small 'Convenience' that makes this
        // method look less ridiculous than it already does.)
        //
        // 'deepEquals(...)' actually checks the entire contents of two array's for equality.

        return
        
            // Image-URL (very common)
            Objects.equals(this.url, ii.url)

            // Base-64 Image Stuff (rare, but not impossible)
            &&  (this.isB64EncodedImage == ii.isB64EncodedImage)
            &&  (Objects.equals(this.imageFormatStr, ii.imageFormatStr))
            &&  (Objects.equals(this.b64EncodedImage, ii.b64EncodedImage))

            // The actual downloaded and converted images, themselves
            &&  Objects.deepEquals(this.imgByteArr, ii.imgByteArr)
            &&  Objects.equals(this.bufferedImage, ii.bufferedImage)

            // Image-Width, Image-Height
            &&  (this.width == ii.width)
            &&  (this.height == ii.height)

            // URL-Aquired Extension & Ultimately-Decided-Upon Extension
            &&  Objects.equals(this.guessedExtension, ii.guessedExtension)
            &&  Objects.equals(this.actualExtension, ii.actualExtension)

            // class 'Results' Array-Counters (index-pointers)
            &&  (this.iteratorCounter == ii.iteratorCounter)
            &&  (this.successCounter == ii.successCounter)

            // This is the lone / only 'non-final' field
            &&  Objects.equals(this.fileName, ii.fileName);
    }

    /**
     * Java's hash-code requirement.  The code is computed by summing the first 15
     * {@link #imgByteArr} array elements.
     * 
     * @return A hash-code that may be used when storing this node in a java sorted-collection.
     */
    public int hashCode()
    {
       if (url != null) return url.toString().hashCode();

       int sum = 0;

       for (int i=0; (i < 15) && (i < imgByteArr.length); i++) sum += imgByteArr[i];

       return sum;
    }
}