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}