Metadata.java
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  */
020 package io.jenetics.jpx;
021 
022 import static java.util.Objects.hash;
023 import static io.jenetics.jpx.Lists.copyOf;
024 import static io.jenetics.jpx.TimeFormat.format;
025 
026 import java.io.DataInput;
027 import java.io.DataOutput;
028 import java.io.IOException;
029 import java.io.InvalidObjectException;
030 import java.io.ObjectInputStream;
031 import java.io.Serial;
032 import java.io.Serializable;
033 import java.time.Instant;
034 import java.util.ArrayList;
035 import java.util.List;
036 import java.util.Objects;
037 import java.util.Optional;
038 import java.util.function.Function;
039 
040 import 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  */
059 public 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 outthrows 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 inthrows 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 }