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 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 }
|