001/*
002 * Java GPX Library (jpx-3.1.0).
003 * Copyright (c) 2016-2023 Franz Wilhelmstötter
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 *
017 * Author:
018 *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019 */
020package io.jenetics.jpx;
021
022import static java.util.Objects.hash;
023import static io.jenetics.jpx.Lists.copyOf;
024import static io.jenetics.jpx.TimeFormat.format;
025
026import java.io.DataInput;
027import java.io.DataOutput;
028import java.io.IOException;
029import java.io.InvalidObjectException;
030import java.io.ObjectInputStream;
031import java.io.Serial;
032import java.io.Serializable;
033import java.time.Instant;
034import java.util.ArrayList;
035import java.util.List;
036import java.util.Objects;
037import java.util.Optional;
038import java.util.function.Function;
039
040import org.w3c.dom.Document;
041
042/**
043 * Information about the GPX file, author, and copyright restrictions goes in
044 * the metadata section. Providing rich, meaningful information about your GPX
045 * files allows others to search for and use your GPS data.
046 * <p>
047 * Creating a GPX object with one track-segment and 3 track-points:
048 * <pre>{@code
049 * final Metadata gpx = Metadata.builder()
050 *     .author("Franz Wilhelmstötter")
051 *     .addLink(Link.of("http://jenetics.io"))
052 *     .build();
053 * }</pre>
054 *
055 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
056 * @version 3.0
057 * @since 1.0
058 */
059public final class Metadata implements Serializable {
060
061        @Serial
062        private static final long serialVersionUID = 2L;
063
064        private final String _name;
065        private final String _description;
066        private final Person _author;
067        private final Copyright _copyright;
068        private final List<Link> _links;
069        private final Instant _time;
070        private final String _keywords;
071        private final Bounds _bounds;
072        private final Document _extensions;
073
074        /**
075         * Create a new {@code Metadata} object with the given parameters.
076         *
077         * @param name the name of the GPX file
078         * @param description a description of the contents of the GPX file
079         * @param author the person or organization who created the GPX file
080         * @param copyright copyright and license information governing use of the
081         *        file
082         * @param links URLs associated with the location described in the file
083         * @param time the creation date of the file
084         * @param keywords keywords associated with the file. Search engines or
085         *        databases can use this information to classify the data.
086         * @param bounds minimum and maximum coordinates which describe the extent
087         *        of the coordinates in the file
088         * @param extensions the XML extensions document
089         */
090        private Metadata(
091                final String name,
092                final String description,
093                final Person author,
094                final Copyright copyright,
095                final List<Link> links,
096                final Instant time,
097                final String keywords,
098                final Bounds bounds,
099                final Document extensions
100        ) {
101                _name = name;
102                _description = description;
103                _author = author;
104                _copyright = copyright;
105                _links = copyOf(links);
106                _time = time;
107                _keywords = keywords;
108                _bounds = bounds;
109                _extensions = extensions;
110        }
111
112        /**
113         * Return the name of the GPX file.
114         *
115         * @return the name of the GPX file
116         */
117        public Optional<String> getName() {
118                return Optional.ofNullable(_name);
119        }
120
121        /**
122         * Return a description of the contents of the GPX file.
123         *
124         * @return a description of the contents of the GPX file
125         */
126        public Optional<String> getDescription() {
127                return Optional.ofNullable(_description);
128        }
129
130        /**
131         * Return the person or organization who created the GPX file.
132         *
133         * @return the person or organization who created the GPX file
134         */
135        public Optional<Person> getAuthor() {
136                return Optional.ofNullable(_author);
137        }
138
139        /**
140         * Return the copyright and license information governing use of the file.
141         *
142         * @return the copyright and license information governing use of the file
143         */
144        public Optional<Copyright> getCopyright() {
145                return Optional.ofNullable(_copyright);
146        }
147
148        /**
149         * Return the URLs associated with the location described in the file. The
150         * returned list immutable.
151         *
152         * @return the URLs associated with the location described in the file
153         */
154        public List<Link> getLinks() {
155                return _links;
156        }
157
158        /**
159         * Return the creation date of the file.
160         *
161         * @return the creation date of the file
162         */
163        public Optional<Instant> getTime() {
164                return Optional.ofNullable(_time);
165        }
166
167        /**
168         * Return the keywords associated with the file. Search engines or databases
169         * can use this information to classify the data.
170         *
171         * @return the keywords associated with the file
172         */
173        public Optional<String> getKeywords() {
174                return Optional.ofNullable(_keywords);
175        }
176
177        /**
178         * Return the minimum and maximum coordinates which describe the extent of
179         * the coordinates in the file.
180         *
181         * @return the minimum and maximum coordinates which describe the extent of
182         *         the coordinates in the file
183         */
184        public Optional<Bounds> getBounds() {
185                return Optional.ofNullable(_bounds);
186        }
187
188        /**
189         * Return the (cloned) extensions document. The root element of the returned
190         * document has the name {@code extensions}.
191         * <pre>{@code
192         * <extensions>
193         *     ...
194         * </extensions>
195         * }</pre>
196         *
197         * @since 1.5
198         *
199         * @return the extensions document
200         * @throws org.w3c.dom.DOMException if the document could not be cloned,
201         *         because of an erroneous XML configuration
202         */
203        public Optional<Document> getExtensions() {
204                return Optional.ofNullable(_extensions).map(XML::clone);
205        }
206
207        /**
208         * Convert the <em>immutable</em> metadata object into a <em>mutable</em>
209         * builder initialized with the current metadata values.
210         *
211         * @since 1.1
212         *
213         * @return a new metadata builder initialized with the values of {@code this}
214         *         metadata
215         */
216        public Builder toBuilder() {
217                return builder()
218                        .name(_name)
219                        .desc(_description)
220                        .author(_author)
221                        .copyright(_copyright)
222                        .links(_links)
223                        .time(_time)
224                        .keywords(_keywords)
225                        .bounds(_bounds)
226                        .extensions(_extensions);
227        }
228
229        /**
230         * Return {@code true} if all metadata properties are {@code null} or empty.
231         *
232         * @return {@code true} if all metadata properties are {@code null} or empty
233         */
234        public boolean isEmpty() {
235                return _name == null &&
236                        _description == null &&
237                        (_author == null || _author.isEmpty()) &&
238                        _copyright == null &&
239                        _links.isEmpty() &&
240                        _time == null &&
241                        _keywords == null &&
242                        _bounds == null &&
243                        _extensions == null;
244        }
245
246        /**
247         * Return {@code true} if not all metadata properties are {@code null} or empty.
248         *
249         * @since 1.1
250         *
251         * @return {@code true} if not all metadata properties are {@code null} or empty
252         */
253        public boolean nonEmpty() {
254                return !isEmpty();
255        }
256
257        @Override
258        public int hashCode() {
259                return hash(
260                        _name,
261                        _description,
262                        _author,
263                        _copyright,
264                        Lists.hashCode(_links),
265                        Objects.hashCode(_time),
266                        _keywords,
267                        _bounds
268                );
269        }
270
271        @Override
272        public boolean equals(final Object obj) {
273                return obj == this ||
274                        obj instanceof Metadata meta &&
275                        Objects.equals(meta._name, _name) &&
276                        Objects.equals(meta._description, _description) &&
277                        Objects.equals(meta._author, _author) &&
278                        Objects.equals(meta._copyright, _copyright) &&
279                        Lists.equals(meta._links, _links) &&
280                        Objects.equals(meta._time, _time) &&
281                        Objects.equals(meta._keywords, _keywords) &&
282                        Objects.equals(meta._bounds, _bounds);
283        }
284
285        /**
286         * Builder class for creating immutable {@code Metadata} objects.
287         * <p>
288         * Creating a GPX object with one track-segment and 3 track-points:
289         * <pre>{@code
290         * final Metadata gpx = Metadata.builder()
291         *     .author("Franz Wilhelmstötter")
292         *     .addLink(Link.of("http://jenetics.io"))
293         *     .build();
294         * }</pre>
295         */
296        public static final class Builder {
297                private String _name;
298                private String _description;
299                private Person _author;
300                private Copyright _copyright;
301                private final List<Link> _links = new ArrayList<>();
302                private Instant _time;
303                private String _keywords;
304                private Bounds _bounds;
305                private Document _extensions;
306
307                private Builder() {
308                }
309
310                /**
311                 * Adds the content of a given {@code Metadata} object.
312                 *
313                 * @param metadata the metadata content
314                 * @return {@code this} {@code Builder} for method chaining
315                 */
316                public Builder metadata(final Metadata metadata) {
317                        _name = metadata._name;
318                        _description = metadata._description;
319                        _author = metadata._author;
320                        _copyright = metadata._copyright;
321                        Lists.copyTo(metadata._links, _links);
322                        _time = metadata._time;
323                        _keywords = metadata._keywords;
324                        _bounds = metadata._bounds;
325                        _extensions = metadata._extensions;
326
327                        return this;
328                }
329
330                /**
331                 * Set the metadata name.
332                 *
333                 * @param name the metadata name
334                 * @return {@code this} {@code Builder} for method chaining
335                 */
336                public Builder name(final String name) {
337                        _name = name;
338                        return this;
339                }
340
341                /**
342                 * Return the current name.
343                 *
344                 * @since 1.3
345                 *
346                 * @return the current name
347                 */
348                public Optional<String> name() {
349                        return Optional.ofNullable(_name);
350                }
351
352                /**
353                 * Set the metadata description.
354                 *
355                 * @param description the metadata description
356                 * @return {@code this} {@code Builder} for method chaining
357                 */
358                public Builder desc(final String description) {
359                        _description = description;
360                        return this;
361                }
362
363                /**
364                 * Return the current description.
365                 *
366                 * @since 1.3
367                 *
368                 * @return the current description
369                 */
370                public Optional<String> desc() {
371                        return Optional.ofNullable(_description);
372                }
373
374                /**
375                 * Set the metadata author.
376                 *
377                 * @param author the metadata author
378                 * @return {@code this} {@code Builder} for method chaining
379                 */
380                public Builder author(final Person author) {
381                        _author = author;
382                        return this;
383                }
384
385                /**
386                 * Set the metadata author.
387                 *
388                 * @param author the metadata author
389                 * @return {@code this} {@code Builder} for method chaining
390                 */
391                public Builder author(final String author) {
392                        return author != null ? author(Person.of(author)) : null;
393                }
394
395                /**
396                 * Return the current author.
397                 *
398                 * @since 1.3
399                 *
400                 * @return the current author
401                 */
402                public Optional<Person> author() {
403                        return Optional.ofNullable(_author);
404                }
405
406                /**
407                 * Set the copyright info.
408                 *
409                 * @param copyright the copyright info
410                 * @return {@code this} {@code Builder} for method chaining
411                 */
412                public Builder copyright(final Copyright copyright) {
413                        _copyright = copyright;
414                        return this;
415                }
416
417                /**
418                 * Return the current copyright info.
419                 *
420                 * @since 1.3
421                 *
422                 * @return the current copyright info
423                 */
424                public Optional<Copyright> copyright() {
425                        return Optional.ofNullable(_copyright);
426                }
427
428                /**
429                 * Set the metadata links.
430                 *
431                 * @param links the metadata links
432                 * @return {@code this} {@code Builder} for method chaining
433                 */
434                public Builder links(final List<Link> links) {
435                        Lists.copyTo(links, _links);
436                        return this;
437                }
438
439                /**
440                 * Add the given {@code link} to the metadata
441                 *
442                 * @param link the link to add to the metadata
443                 * @return {@code this} {@code Builder} for method chaining
444                 */
445                public Builder addLink(final Link link) {
446                        if (link != null) {
447                                _links.add(link);
448                        }
449                        return this;
450                }
451
452                /**
453                 * Add the given {@code link} to the metadata
454                 *
455                 * @param href the link to add to the metadata
456                 * @return {@code this} {@code Builder} for method chaining
457                 * @throws IllegalArgumentException if the given {@code href} is not a
458                 *         valid URL
459                 */
460                public Builder addLink(final String href) {
461                        if (href != null) {
462                                addLink(Link.of(href));
463                        }
464                        return this;
465                }
466
467                /**
468                 * Return the current links.
469                 *
470                 * @since 1.3
471                 *
472                 * @return the current links
473                 */
474                public List<Link> links() {
475                        return new NonNullList<>(_links);
476                }
477
478                /**
479                 * Set the time of the metadata
480                 *
481                 * @param time the time of the metadata
482                 * @return {@code this} {@code Builder} for method chaining
483                 */
484                public Builder time(final Instant time) {
485                        _time = time;
486                        return this;
487                }
488
489                /**
490                 * Set the time of the metadata.
491                 *
492                 * @param millis the instant to create the metadata time from
493                 * @return {@code this} {@code Builder} for method chaining
494                 */
495                public Builder time(final long millis) {
496                        _time = Instant.ofEpochMilli(millis);
497                        return this;
498                }
499
500                /**
501                 * Return the currently set time.
502                 *
503                 * @since 1.3
504                 *
505                 * @return the currently set time
506                 */
507                public Optional<Instant> time() {
508                        return Optional.ofNullable(_time);
509                }
510
511                /**
512                 * Set the metadata keywords.
513                 *
514                 * @param keywords the metadata keywords
515                 * @return {@code this} {@code Builder} for method chaining
516                 */
517                public Builder keywords(final String keywords) {
518                        _keywords = keywords;
519                        return this;
520                }
521
522                /**
523                 * Return the current keywords.
524                 *
525                 * @since 1.3
526                 *
527                 * @return the current keywords
528                 */
529                public Optional<String> keywords() {
530                        return Optional.ofNullable(_keywords);
531                }
532
533                /**
534                 * Set the GPX bounds.
535                 *
536                 * @param bounds the GPX bounds
537                 * @return {@code this} {@code Builder} for method chaining
538                 */
539                public Builder bounds(final Bounds bounds) {
540                        _bounds = bounds;
541                        return this;
542                }
543
544                /**
545                 * Return the current bounds.
546                 *
547                 * @since 1.3
548                 *
549                 * @return the current bounds
550                 */
551                public Optional<Bounds> bounds() {
552                        return Optional.ofNullable(_bounds);
553                }
554
555                /**
556                 * Sets the extensions object, which may be {@code null}. The root
557                 * element of the extensions document must be {@code extensions}.
558                 * <pre>{@code
559                 * <extensions>
560                 *     ...
561                 * </extensions>
562                 * }</pre>
563                 *
564                 * @since 1.5
565                 *
566                 * @param extensions the document
567                 * @return {@code this} {@code Builder} for method chaining
568                 * @throws IllegalArgumentException if the root element is not the
569                 *         an {@code extensions} node
570                 */
571                public Builder extensions(final Document extensions) {
572                        _extensions = XML.checkExtensions(extensions);
573                        return this;
574                }
575
576                /**
577                 * Return the current extensions
578                 *
579                 * @since 1.5
580                 *
581                 * @return the extensions document
582                 */
583                public Optional<Document> extensions() {
584                        return Optional.ofNullable(_extensions);
585                }
586
587                /**
588                 * Create an immutable {@code Metadata} object from the current builder
589                 * state.
590                 *
591                 * @return an immutable {@code Metadata} object from the current builder
592                 *         state
593                 */
594                public Metadata build() {
595                        return new Metadata(
596                                _name,
597                                _description,
598                                _author,
599                                _copyright,
600                                _links,
601                                _time,
602                                _keywords,
603                                _bounds,
604                                _extensions
605                        );
606                }
607        }
608
609        /**
610         * Return a new {@code Metadata} builder.
611         *
612         * @return a new {@code Metadata} builder
613         */
614        public static Builder builder() {
615                return new Builder();
616        }
617
618
619        /* *************************************************************************
620         *  Static object creation methods
621         * ************************************************************************/
622
623        /**
624         * Create a new {@code Metadata} object with the given parameters.
625         *
626         * @since 1.5
627         *
628         * @param name the name of the GPX file
629         * @param description a description of the contents of the GPX file
630         * @param author the person or organization who created the GPX file
631         * @param copyright copyright and license information governing use of the
632         *        file
633         * @param links URLs associated with the location described in the file
634         * @param time the creation date of the file
635         * @param keywords keywords associated with the file. Search engines or
636         *        databases can use this information to classify the data.
637         * @param bounds minimum and maximum coordinates which describe the extent
638         *        of the coordinates in the file
639         * @param extensions the extensions document
640         * @return a new {@code Metadata} object with the given parameters
641         * @throws NullPointerException if the given {@code links} sequence is
642         *        {@code null}
643         */
644        public static Metadata of(
645                final String name,
646                final String description,
647                final Person author,
648                final Copyright copyright,
649                final List<Link> links,
650                final Instant time,
651                final String keywords,
652                final Bounds bounds,
653                final Document extensions
654        ) {
655                return new Metadata(
656                        name,
657                        description,
658                        author == null || author.isEmpty() ? null : author,
659                        copyright,
660                        links,
661                        time,
662                        keywords,
663                        bounds,
664                        XML.extensions(XML.clone(extensions))
665                );
666        }
667
668        /**
669         * Create a new {@code Metadata} object with the given parameters.
670         *
671         * @param name the name of the GPX file
672         * @param description a description of the contents of the GPX file
673         * @param author the person or organization who created the GPX file
674         * @param copyright copyright and license information governing use of the
675         *        file
676         * @param links URLs associated with the location described in the file
677         * @param time the creation date of the file
678         * @param keywords keywords associated with the file. Search engines or
679         *        databases can use this information to classify the data.
680         * @param bounds minimum and maximum coordinates which describe the extent
681         *        of the coordinates in the file
682         * @return a new {@code Metadata} object with the given parameters
683         * @throws NullPointerException if the given {@code links} sequence is
684         *        {@code null}
685         */
686        public static Metadata of(
687                final String name,
688                final String description,
689                final Person author,
690                final Copyright copyright,
691                final List<Link> links,
692                final Instant time,
693                final String keywords,
694                final Bounds bounds
695        ) {
696                return of(
697                        name,
698                        description,
699                        author,
700                        copyright,
701                        links,
702                        time,
703                        keywords,
704                        bounds,
705                        null
706                );
707        }
708
709
710        /* *************************************************************************
711         *  Java object serialization
712         * ************************************************************************/
713
714        @Serial
715        private Object writeReplace() {
716                return new SerialProxy(SerialProxy.METADATA, this);
717        }
718
719        @Serial
720        private void readObject(final ObjectInputStream stream)
721                throws InvalidObjectException
722        {
723                throw new InvalidObjectException("Serialization proxy required.");
724        }
725
726        void write(final DataOutput out) throws IOException {
727                IO.writeNullableString(_name, out);
728                IO.writeNullableString(_description, out);
729                IO.writeNullable(_author, Person::write, out);
730                IO.writeNullable(_copyright, Copyright::write, out);
731                IO.writes(_links, Link::write, out);
732                IO.writeNullable(_time, Instants::write, out);
733                IO.writeNullableString(_keywords, out);
734                IO.writeNullable(_bounds, Bounds::write, out);
735                IO.writeNullable(_extensions, IO::write, out);
736        }
737
738        static Metadata read(final DataInput in) throws IOException {
739                return new Metadata(
740                        IO.readNullableString(in),
741                        IO.readNullableString(in),
742                        IO.readNullable(Person::read, in),
743                        IO.readNullable(Copyright::read, in),
744                        IO.reads(Link::read, in),
745                        IO.readNullable(Instants::read, in),
746                        IO.readNullableString(in),
747                        IO.readNullable(Bounds::read, in),
748                        IO.readNullable(IO::readDoc, in)
749                );
750        }
751
752
753        /* *************************************************************************
754         *  XML stream object serialization
755         * ************************************************************************/
756
757        static XMLWriter<Metadata>
758        writer(final Function<? super Number, String> formatter) {
759                return XMLWriter.elem("metadata",
760                        XMLWriter.elem("name").map(md -> md._name),
761                        XMLWriter.elem("desc").map(md -> md._description),
762                        Person.writer("author").map(md -> md._author),
763                        Copyright.WRITER.map(md -> md._copyright),
764                        XMLWriter.elems(Link.WRITER).map(md -> md._links),
765                        XMLWriter.elem("time").map(md -> format(md._time)),
766                        XMLWriter.elem("keywords").map(md -> md._keywords),
767                        Bounds.writer(formatter).map(md -> md._bounds),
768                        XMLWriter.doc("extensions").map(md -> md._extensions)
769                );
770        }
771
772        @SuppressWarnings("unchecked")
773        static final XMLReader<Metadata> READER = XMLReader.elem(
774                v -> {
775                        final Metadata metadata = new Metadata(
776                                (String)v[0],
777                                (String)v[1],
778                                (Person)v[2],
779                                (Copyright)v[3],
780                                (List<Link>)v[4],
781                                (Instant)v[5],
782                                (String)v[6],
783                                (Bounds)v[7],
784                                XML.extensions((Document)v[8])
785                        );
786
787                        return metadata.isEmpty() ? null : metadata;
788                },
789                "metadata",
790                XMLReader.elem("name"),
791                XMLReader.elem("desc"),
792                Person.reader("author"),
793                Copyright.READER,
794                XMLReader.elems(Link.READER),
795                XMLReader.elem("time").map(TimeFormat::parse),
796                XMLReader.elem("keywords"),
797                Bounds.READER,
798                XMLReader.doc("extensions")
799        );
800
801}