diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 3b6d94c..c97f14e 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,11 +1,12 @@
module org.spongepowered.plugin.metadata {
exports org.spongepowered.plugin.metadata;
- exports org.spongepowered.plugin.metadata.builtin;
- exports org.spongepowered.plugin.metadata.builtin.model;
exports org.spongepowered.plugin.metadata.model;
- exports org.spongepowered.plugin.metadata.util;
+ exports org.spongepowered.plugin.metadata.builtin;
+ exports org.spongepowered.plugin.metadata.builtin.adapter;
+ exports org.spongepowered.plugin.metadata.builtin.adapter.model;
+ exports org.spongepowered.plugin.metadata.builtin.adapter.version;
requires transitive com.google.gson;
requires static transitive org.checkerframework.checker.qual;
requires static transitive maven.artifact; // Generated module name, may change?
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/Constants.java b/src/main/java/org/spongepowered/plugin/metadata/Constants.java
index 282d645..1334eb8 100644
--- a/src/main/java/org/spongepowered/plugin/metadata/Constants.java
+++ b/src/main/java/org/spongepowered/plugin/metadata/Constants.java
@@ -35,13 +35,13 @@ public final class Constants {
*
Must be between 2 and 64 characters in length
*
Must start with a lower case letter (a-z)
*
May only contain a mix of lower case letters (a-z),
- * numbers (0-9), dashes (-), and underscores (_)
+ * numbers (0-9) and underscores (_)
*
*/
- public static final Pattern VALID_ID_PATTERN = Pattern.compile("^[a-z][a-z0-9-_]{1,63}$");
+ public static final Pattern VALID_ID_PATTERN = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
public static final String INVALID_ID_REQUIREMENTS_MESSAGE = "IDs can be between 2 and 64 characters long and must start with a lower case"
- + " letter, followed by any mix of lower case letters (a-z), numbers (0-9), dashes (-) and underscores (_).";
+ + " letter, followed by any mix of lower case letters (a-z), numbers (0-9) and underscores (_).";
private Constants() {
}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/Container.java b/src/main/java/org/spongepowered/plugin/metadata/Container.java
deleted file mode 100644
index 4caa528..0000000
--- a/src/main/java/org/spongepowered/plugin/metadata/Container.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * This file is part of plugin-meta, licensed under the MIT License (MIT).
- *
- * Copyright (c) SpongePowered
- * Copyright (c) contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package org.spongepowered.plugin.metadata;
-
-import org.apache.maven.artifact.Artifact;
-import org.spongepowered.plugin.metadata.model.ContainerLoader;
-
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * A container joins together {@link Inheritable global metadata} with specific one or more
- * {@link PluginMetadata plugin metadata}.
- *
- * How the vendor utilizes this concept is largely left up to their discretion. A
- * typical use case would be to load a file as a container.
- *
- * @see org.spongepowered.plugin.metadata.builtin.MetadataContainer MetadataContainer, for a generic implementation
- */
-public interface Container {
-
- /**
- * @return The {@link ContainerLoader loader}.
- */
- ContainerLoader loader();
-
- /**
- * Gets the {@link String license} of the data within this container.
- *
- * Consult the vendor for how this field is used.
- *
- * In the generic implementation, a license's name is expected.
- *
- * Notable examples include MIT or All Rights Reserved.
- *
- * @return The license
- */
- String license();
-
- /**
- * Gets the {@link Artifact mappings} that code within this container might be written in.
- *
- * The format of this string should be in maven dependency format (group:artifact:version).
- *
- * Consult the vendor for how this field is used. As an example, that entity could use this
- * purely for information purposes or go farther and perform artifact remapping from these
- * mappings to another.
- *
- * @return The mappings or {@link Optional#empty()} otherwise
- */
- Optional mappings();
-
- /**
- * @return The {@link Inheritable global metadata} or {@link Optional#empty()} otherwise
- */
- Optional globalMetadata();
-
- /**
- * Gets a {@link PluginMetadata plugin metadata} by its {@link String id}.
- *
- * This maps to {@link PluginMetadata#id()}.
- *
- * @param id The id
- * @return The plugin metadata or {@link Optional#empty()} otherwise
- */
- Optional metadata(String id);
-
- /**
- * @return All of the {@link PluginMetadata plugin metadata} as an unmodifiable {@link Set}.
- */
- Set metadata();
-}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/Inheritable.java b/src/main/java/org/spongepowered/plugin/metadata/Inheritable.java
deleted file mode 100644
index ac35191..0000000
--- a/src/main/java/org/spongepowered/plugin/metadata/Inheritable.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * This file is part of plugin-meta, licensed under the MIT License (MIT).
- *
- * Copyright (c) SpongePowered
- * Copyright (c) contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package org.spongepowered.plugin.metadata;
-
-import org.apache.maven.artifact.versioning.ArtifactVersion;
-import org.spongepowered.plugin.metadata.model.PluginBranding;
-import org.spongepowered.plugin.metadata.model.PluginContributor;
-import org.spongepowered.plugin.metadata.model.PluginDependency;
-import org.spongepowered.plugin.metadata.model.PluginLinks;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * Represents metadata that is meant to be inherited/overwritten, per contract.
- *
- * @see org.spongepowered.plugin.metadata.builtin.StandardInheritable StandardInheritable, for a generic implementation
- * @see PluginMetadata PluginMetadata, for specific "plugin" metadata
- */
-public interface Inheritable {
-
- /**
- * @return The {@link ArtifactVersion version}.
- */
- ArtifactVersion version();
-
- /**
- * @return The {@link PluginBranding branding}.
- */
- PluginBranding branding();
-
- /**
- * @return The {@link PluginLinks links} to various web resources.
- */
- PluginLinks links();
-
- /**
- * @return The {@link PluginContributor contributors} as an unmodifiable {@link List}.
- */
- List contributors();
-
- /**
- * Gets the {@link PluginDependency plugin dependency} by {@link String id}.
- *
- * This maps to {@link PluginDependency#id()}.
- * @param id The id
- * @return The dependency or {@link Optional#empty()} otherwise.
- */
- Optional dependency(String id);
-
- /**
- * @return The {@link PluginDependency dependencies} as an unmodifiable {@link Set}.
- */
- Set dependencies();
-
- /**
- * Gets the {@link T property} by {@link String key}.
- *
- * @param key The key
- * @param The type
- * @return The property or {@link Optional#empty()} otherwise.
- */
- Optional property(String key);
-
- /**
- * @return The properties as an unmodifiable {@link Map}.
- */
- Map properties();
-}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java b/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java
index caf8c12..ea6d4e0 100644
--- a/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java
+++ b/src/main/java/org/spongepowered/plugin/metadata/PluginMetadata.java
@@ -24,20 +24,25 @@
*/
package org.spongepowered.plugin.metadata;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.spongepowered.plugin.metadata.model.PluginBranding;
+import org.spongepowered.plugin.metadata.model.PluginContributor;
+import org.spongepowered.plugin.metadata.model.PluginDependency;
+import org.spongepowered.plugin.metadata.model.PluginEntrypoints;
+import org.spongepowered.plugin.metadata.model.PluginLinks;
+import org.spongepowered.plugin.metadata.model.PluginLoaderSpecification;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
/**
- * Represents specific, unique metadata to a plugin.
+ * Represents the metadata of a plugin.
*
- * @see Inheritable Inheritable, for metadata that might be shared between multiple plugin metadata
* @see org.spongepowered.plugin.metadata.builtin.StandardPluginMetadata StandardPluginMetadata, for a generic implementation
*/
-public interface PluginMetadata extends Inheritable {
-
- /**
- * @return The {@link Container container}.
- */
- Container container();
+public interface PluginMetadata {
/**
* Gets the {@link String id}.
@@ -48,21 +53,26 @@ public interface PluginMetadata extends Inheritable {
*
Must be between 2 and 64 characters in length
*
Must start with a lower case letter (a-z)
*
May only contain a mix of lower case letters (a-z),
- * numbers (0-9), dashes (-), and underscores (_)
+ * numbers (0-9) and underscores (_)
*
* @return The id
*/
String id();
/**
- * Gets the {@link String entrypoint}.
- *
- * Consult the vendor for how this field is used. As an example, this could be
- * the name of a module or a fully realized path to a discrete class.
- *
- * @return The entrypoint
+ * @return The {@link PluginEntrypoints entrypoints}
*/
- String entrypoint();
+ PluginEntrypoints entrypoints();
+
+ /**
+ * @return The {@link ArtifactVersion version}.
+ */
+ ArtifactVersion version();
+
+ /**
+ * @return The {@link PluginLoaderSpecification loader}.
+ */
+ PluginLoaderSpecification loader();
/**
* @return The {@link String name} or {@link Optional#empty()} otherwise.
@@ -73,4 +83,57 @@ public interface PluginMetadata extends Inheritable {
* @return The {@link String description} or {@link Optional#empty()} otherwise.
*/
Optional description();
+
+ /**
+ * Gets the {@link String license} of the data within this container.
+ *
+ * Notable examples include MIT or All Rights Reserved.
+ *
+ * @return The license
+ */
+ Optional license();
+
+ /**
+ * @return The {@link PluginBranding branding}.
+ */
+ PluginBranding branding();
+
+ /**
+ * @return The {@link PluginLinks links} to various web resources.
+ */
+ PluginLinks links();
+
+ /**
+ * @return The {@link PluginContributor contributors} as an unmodifiable {@link List}.
+ */
+ List contributors();
+
+ /**
+ * Gets the {@link PluginDependency plugin dependency} by {@link String id}.
+ *
+ * This maps to {@link PluginDependency#id()}.
+ * @param id The id
+ * @return The dependency or {@link Optional#empty()} otherwise.
+ */
+ Optional dependency(String id);
+
+ /**
+ * @return The {@link PluginDependency dependencies} as an unmodifiable {@link Collection}.
+ */
+ Collection dependencies();
+
+ /**
+ * Gets the {@link T property} by {@link String key}.
+ *
+ * @param key The key
+ * @param The type
+ * @return The property or {@link Optional#empty()} otherwise.
+ */
+ Optional property(String key);
+
+ /**
+ * @return The properties as an unmodifiable {@link Map}.
+ */
+ Map properties();
+
}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java
new file mode 100644
index 0000000..385f0bd
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/InheritableMetadata.java
@@ -0,0 +1,335 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin;
+
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.plugin.metadata.model.PluginBranding;
+import org.spongepowered.plugin.metadata.model.PluginContributor;
+import org.spongepowered.plugin.metadata.model.PluginDependency;
+import org.spongepowered.plugin.metadata.model.PluginLinks;
+import org.spongepowered.plugin.metadata.model.PluginLoaderSpecification;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+public final class InheritableMetadata {
+ private static final InheritableMetadata NONE = new InheritableMetadata();
+
+ private final @Nullable ArtifactVersion version;
+ private final @Nullable PluginLoaderSpecification loader;
+ private final @Nullable String name;
+ private final @Nullable String description;
+ private final @Nullable String license;
+ private final PluginBranding branding;
+ private final PluginLinks links;
+ private final List contributors;
+ private final Map dependencies;
+ private final Map properties;
+
+ private InheritableMetadata() {
+ this.version = null;
+ this.loader = null;
+ this.name = null;
+ this.description = null;
+ this.license = null;
+ this.branding = PluginBranding.none();
+ this.links = PluginLinks.none();
+ this.contributors = List.of();
+ this.dependencies = Map.of();
+ this.properties = Map.of();
+ }
+
+ private InheritableMetadata(final Builder builder) {
+ this.version = builder.version;
+ this.loader = builder.loader;
+ this.name = builder.name;
+ this.description = builder.description;
+ this.license = builder.license;
+ this.branding = builder.branding;
+ this.links = builder.links;
+ this.contributors = List.copyOf(builder.contributors);
+ this.dependencies = Collections.unmodifiableMap(new LinkedHashMap<>(builder.dependencies));
+ this.properties = Collections.unmodifiableMap(new LinkedHashMap<>(builder.properties));
+ }
+
+ public Optional version() {
+ return Optional.ofNullable(this.version);
+ }
+
+ public Optional loader() {
+ return Optional.ofNullable(this.loader);
+ }
+
+ public Optional name() {
+ return Optional.ofNullable(this.name);
+ }
+
+ public Optional description() {
+ return Optional.ofNullable(this.description);
+ }
+
+ public Optional license() {
+ return Optional.ofNullable(this.license);
+ }
+
+ public PluginBranding branding() {
+ return this.branding;
+ }
+
+ public PluginLinks links() {
+ return this.links;
+ }
+
+ public List contributors() {
+ return this.contributors;
+ }
+
+ public Map dependencies() {
+ return this.dependencies;
+ }
+
+ public Map properties() {
+ return this.properties;
+ }
+
+ public InheritableMetadata with(final InheritableMetadata override) {
+ return this.toBuilder().with(override).build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+
+ if (!(o instanceof InheritableMetadata other)) {
+ return false;
+ }
+
+ return Objects.equals(this.version, other.version)
+ && Objects.equals(this.loader, other.loader)
+ && Objects.equals(this.name, other.name)
+ && Objects.equals(this.description, other.description)
+ && Objects.equals(this.license, other.license)
+ && this.branding.equals(other.branding)
+ && this.links.equals(other.links)
+ && this.contributors.equals(other.contributors)
+ && this.dependencies.equals(other.dependencies)
+ && this.properties.equals(other.properties);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.version, this.loader, this.name, this.description, this.license,
+ this.branding, this.links, this.contributors, this.dependencies, this.properties);
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", InheritableMetadata.class.getSimpleName() + "[", "]")
+ .add("version=" + this.version)
+ .add("loader=" + this.loader)
+ .add("license=" + this.license)
+ .add("branding=" + this.branding)
+ .add("links=" + this.links)
+ .add("contributors=" + this.contributors)
+ .add("dependencies=" + this.dependencies)
+ .add("properties=" + this.properties)
+ .toString();
+ }
+
+ public InheritableMetadata.Builder toBuilder() {
+ return new InheritableMetadata.Builder().from(this);
+ }
+
+ public static InheritableMetadata.Builder builder() {
+ return new InheritableMetadata.Builder();
+ }
+
+ public static InheritableMetadata none() {
+ return InheritableMetadata.NONE;
+ }
+
+ public static final class Builder {
+ private @Nullable ArtifactVersion version;
+ private @Nullable PluginLoaderSpecification loader;
+ private @Nullable String name;
+ private @Nullable String description;
+ private @Nullable String license;
+ private PluginBranding branding = PluginBranding.none();
+ private PluginLinks links = PluginLinks.none();
+ private final List contributors = new LinkedList<>();
+ private final Map dependencies = new LinkedHashMap<>();
+ private final Map properties = new LinkedHashMap<>();
+
+ private Builder() {
+ }
+
+ public Builder version(final @Nullable ArtifactVersion version) {
+ this.version = version;
+ return this;
+ }
+
+ public Builder loader(final @Nullable PluginLoaderSpecification loader) {
+ this.loader = loader;
+ return this;
+ }
+
+ public Builder name(final @Nullable String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder description(final @Nullable String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder license(final @Nullable String license) {
+ this.license = license;
+ return this;
+ }
+
+ public Builder branding(final PluginBranding branding) {
+ this.branding = Objects.requireNonNull(branding, "branding");
+ return this;
+ }
+
+ public Builder links(final PluginLinks links) {
+ this.links = Objects.requireNonNull(links, "links");
+ return this;
+ }
+
+ public Builder contributors(final Collection contributors) {
+ Objects.requireNonNull(contributors, "contributors");
+ this.contributors.clear();
+ this.contributors.addAll(contributors);
+ return this;
+ }
+
+ public Builder addContributors(final Collection contributors) {
+ this.contributors.addAll(Objects.requireNonNull(contributors, "contributors"));
+ return this;
+ }
+
+ public Builder addContributor(final PluginContributor contributor) {
+ this.contributors.add(Objects.requireNonNull(contributor, "contributor"));
+ return this;
+ }
+
+ public Builder dependencies(final Collection dependencies) {
+ Objects.requireNonNull(dependencies, "dependencies");
+ this.dependencies.clear();
+ return this.addDependencies(dependencies);
+ }
+
+ public Builder addDependencies(final Collection dependencies) {
+ for (final PluginDependency element : Objects.requireNonNull(dependencies, "dependencies")) {
+ this.dependencies.put(Objects.requireNonNull(element, "element").id(), element);
+ }
+ return this;
+ }
+
+ public Builder addDependency(final PluginDependency dependency) {
+ this.dependencies.put(Objects.requireNonNull(dependency, "dependency").id(), dependency);
+ return this;
+ }
+
+ public Builder properties(final Map properties) {
+ Objects.requireNonNull(properties, "properties");
+ this.properties.clear();
+ this.properties.putAll(properties);
+ return this;
+ }
+
+ public Builder addProperties(final Map properties) {
+ this.properties.putAll(Objects.requireNonNull(properties, "properties"));
+ return this;
+ }
+
+ public Builder addProperty(final String key, final Object value) {
+ this.properties.put(Objects.requireNonNull(key, "key"), Objects.requireNonNull(value, "value"));
+ return this;
+ }
+
+ public Builder from(final InheritableMetadata value) {
+ Objects.requireNonNull(value, "value");
+ this.version = value.version;
+ this.loader = value.loader;
+ this.name = value.name;
+ this.description = value.description;
+ this.license = value.license;
+ this.branding = value.branding;
+ this.links = value.links;
+ this.contributors.clear();
+ this.contributors.addAll(value.contributors);
+ this.dependencies.clear();
+ this.dependencies.putAll(value.dependencies);
+ this.properties.clear();
+ this.properties.putAll(value.properties);
+ return this;
+ }
+
+ public Builder with(final InheritableMetadata override) {
+ if (override.version != null) {
+ this.version = override.version;
+ }
+ if (override.loader != null) {
+ this.loader = override.loader;
+ }
+ if (override.name != null) {
+ this.name = override.name;
+ }
+ if (override.description != null) {
+ this.description = override.description;
+ }
+ if (override.license != null) {
+ this.license = override.license;
+ }
+ if (!override.branding.equals(PluginBranding.none())) {
+ this.branding = override.branding;
+ }
+ if (!override.links.equals(PluginLinks.none())) {
+ this.links = override.links;
+ }
+ this.contributors.addAll(override.contributors);
+ this.dependencies.putAll(override.dependencies);
+ this.properties.putAll(override.properties);
+ return this;
+ }
+
+ public InheritableMetadata build() {
+ return new InheritableMetadata(this);
+ }
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataContainer.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataContainer.java
index 6896a56..e71c3d8 100644
--- a/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataContainer.java
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataContainer.java
@@ -24,244 +24,20 @@
*/
package org.spongepowered.plugin.metadata.builtin;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
-import org.apache.maven.artifact.ArtifactUtils;
-import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.spongepowered.plugin.metadata.Container;
-import org.spongepowered.plugin.metadata.Inheritable;
-import org.spongepowered.plugin.metadata.PluginMetadata;
-import org.spongepowered.plugin.metadata.builtin.model.Adapters;
-import org.spongepowered.plugin.metadata.builtin.model.StandardContainerLoader;
-import org.spongepowered.plugin.metadata.util.GsonUtils;
-
-import java.lang.reflect.Type;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.StringJoiner;
-
-public final class MetadataContainer implements Container {
-
- private final String license;
- @Nullable private final String mappings;
- private final StandardContainerLoader loader;
- @Nullable private final Inheritable globalMetadata;
- private final Set metadata = new LinkedHashSet<>();
- private final Map metadataById = new LinkedHashMap<>();
-
- private MetadataContainer(final Builder builder) {
- this.loader = builder.loader;
- this.license = builder.license;
- this.mappings = builder.mappings;
- this.globalMetadata = builder.globalMetadata;
- this.metadata.addAll(builder.metadata);
- for (final StandardPluginMetadata pm : this.metadata) {
- this.metadataById.put(pm.id(), pm);
- pm.setContainer(this);
- }
- }
-
- @Override
- public StandardContainerLoader loader() {
- return this.loader;
- }
-
- @Override
- public String license() {
- return this.license;
- }
-
- @Override
- public Optional mappings() {
- return Optional.ofNullable(this.mappings);
- }
-
- @Override
- public Optional globalMetadata() {
- return Optional.ofNullable(this.globalMetadata);
- }
-
- @Override
- public Optional metadata(final String id) {
- return Optional.ofNullable(this.metadataById.get(Objects.requireNonNull(id, "id")));
- }
-
- @Override
- public Set metadata() {
- return Collections.unmodifiableSet(this.metadata);
- }
-
- @Override
- public String toString() {
- return new StringJoiner(", ", MetadataContainer.class.getSimpleName() + "[", "]")
- .add("loader=" + this.loader)
- .add("license=" + this.license)
- .add("mappings=" + this.mappings)
- .add("globalMetadata=" + this.globalMetadata)
- .toString();
- }
-
- public MetadataContainer.Builder toBuilder() {
- final Builder builder = new Builder();
- builder.loader = this.loader;
- builder.license = this.license;
- builder.mappings = this.mappings;
- builder.globalMetadata = this.globalMetadata;
- builder.metadata.addAll(this.metadata);
- return builder;
- }
-
- public static final class Builder {
-
- final Set metadata = new LinkedHashSet<>();
- @Nullable String license, mappings;
- @Nullable StandardContainerLoader loader;
- @Nullable Inheritable globalMetadata;
-
- public Builder loader(final StandardContainerLoader loader) {
- this.loader = Objects.requireNonNull(loader, "loader");
- return this;
- }
-
- public Builder license(final String license) {
- this.license = Objects.requireNonNull(license, "license");
- return this;
- }
-
- public Builder mappings(@Nullable final String mappings) {
-
- if (mappings != null) {
- final String[] elements = mappings.split("\\.", 3);
- // Triggers their sanity checks
- ArtifactUtils.key(elements[0], elements[1], elements[2]);
- }
-
- this.mappings = mappings;
- return this;
- }
-
- public Builder globalMetadata(final Inheritable globalMetadata) {
- this.globalMetadata = Objects.requireNonNull(globalMetadata, "globalMetadata");
- return this;
- }
-
- public Builder metadata(final List metadata) {
- Objects.requireNonNull(metadata, "metadata");
- this.metadata.clear();
- this.metadata.addAll(metadata);
- return this;
- }
-
- public Builder addMetadata(final List metadata) {
- this.metadata.addAll(Objects.requireNonNull(metadata, "metadata"));
- return this;
- }
-
- public Builder addMetadata(final StandardPluginMetadata metadata) {
- this.metadata.add(Objects.requireNonNull(metadata, "metadata"));
- return this;
- }
-
- public MetadataContainer build() throws IllegalStateException, InvalidVersionSpecificationException {
- Objects.requireNonNull(this.license, "license");
- Objects.requireNonNull(this.loader, "loader");
-
- if (this.metadata.isEmpty()) {
- throw new IllegalStateException("A MetadataHolder must hold at least 1 PluginMetadata!");
- }
-
- return new MetadataContainer(this);
- }
- }
-
- public static final class Serializer implements JsonSerializer, JsonDeserializer {
-
- @Override
- public MetadataContainer deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context)
- throws JsonParseException {
- final JsonObject obj = element.getAsJsonObject();
- if (!obj.has("loader")) {
- throw new MissingRequiredFieldException("loader");
- }
- if (!obj.has("license")) {
- throw new MissingRequiredFieldException("license");
- }
- if (!obj.has("plugins")) {
- throw new MissingRequiredFieldException("plugins");
- }
-
- final Builder builder = new Builder()
- .loader(Adapters.Deserializers.CONTAINER_LOADER.fromJsonTree(obj.get("loader")).build())
- .license(obj.get("license").getAsString());
-
- GsonUtils.consumeIfPresent(obj, "mappings", e -> builder.mappings(e.getAsString()));
-
- final JsonElement globalElement = obj.get("global");
- @Nullable StandardInheritable inheritable = null;
- if (globalElement instanceof JsonObject) {
- inheritable = context.deserialize(globalElement, StandardInheritable.class);
- builder.globalMetadata(inheritable);
- }
- final JsonElement pluginsElement = obj.get("plugins");
- final List pluginObjects = new LinkedList<>();
+public record MetadataContainer(InheritableMetadata global, List plugins) {
- if (pluginsElement.isJsonArray()) {
- for (final JsonElement pluginElement : ((JsonArray) pluginsElement)) {
- if (pluginElement.isJsonObject()) {
- pluginObjects.add((JsonObject) pluginElement);
- }
- }
+ public MetadataContainer {
+ Objects.requireNonNull(global, "global");
+ Objects.requireNonNull(plugins, "plugins");
+ for (final StandardPluginMetadata plugin : plugins) {
+ Objects.requireNonNull(plugin, "plugin");
+ if (!plugin.global().equals(global)) {
+ throw new IllegalArgumentException("global metadata mismatch");
}
-
- if (pluginObjects.isEmpty()) {
- throw new JsonParseException("No plugin metadata has been specified for the 'plugins' tag!");
- }
-
- for (final JsonObject pluginObject : pluginObjects) {
- final StandardPluginMetadata.Builder pluginBuilder = context.deserialize(pluginObject, StandardPluginMetadata.Builder.class);
- if (inheritable != null) {
- pluginBuilder.from(inheritable);
- }
-
- builder.addMetadata(pluginBuilder.build());
- }
-
- try {
- return builder.build();
- } catch (final InvalidVersionSpecificationException e) {
- throw new JsonParseException(e);
- }
- }
-
- @Override
- public JsonElement serialize(final MetadataContainer value, final Type type, final JsonSerializationContext context) {
- final JsonObject obj = new JsonObject();
- obj.add("loader", Adapters.Serializers.CONTAINER_LOADER.toJsonTree(value.loader));
- obj.addProperty("license", value.license);
- GsonUtils.writeIfPresent(obj, "mappings", value.mappings());
-
- final JsonArray plugins = new JsonArray();
- for (final PluginMetadata metadata : value.metadata) {
- plugins.add(context.serialize(metadata, StandardPluginMetadata.class));
- }
- obj.add("plugins", plugins);
-
- return obj;
}
+ plugins = List.copyOf(plugins);
}
}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java
index 05f624c..584d60a 100644
--- a/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/MetadataParser.java
@@ -28,6 +28,16 @@
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.spongepowered.plugin.metadata.builtin.adapter.InheritableMetadataAdapter;
+import org.spongepowered.plugin.metadata.builtin.adapter.MetadataContainerAdapter;
+import org.spongepowered.plugin.metadata.builtin.adapter.StandardPluginMetadataBuilderDeserializer;
+import org.spongepowered.plugin.metadata.builtin.adapter.StandardPluginMetadataSerializer;
+import org.spongepowered.plugin.metadata.builtin.adapter.model.*;
+import org.spongepowered.plugin.metadata.builtin.adapter.version.ArtifactVersionAdapter;
+import org.spongepowered.plugin.metadata.builtin.adapter.version.VersionRangeAdapter;
+import org.spongepowered.plugin.metadata.model.*;
import java.io.BufferedReader;
import java.io.IOException;
@@ -35,38 +45,69 @@
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.Objects;
+import java.util.Set;
public final class MetadataParser {
+ private static final Gson GSON = new GsonBuilder()
+ .registerTypeAdapter(PluginLoaderSpecification.class, new PluginLoaderSpecificationAdapter())
+ .registerTypeAdapter(PluginBranding.class, new PluginBrandingAdapter())
+ .registerTypeAdapter(PluginContributor.class, new PluginContributorAdapter())
+ .registerTypeAdapter(PluginDependency.class, new PluginDependencyAdapter())
+ .registerTypeAdapter(PluginLinks.class, new PluginLinksAdapter())
+ .registerTypeAdapter(ArtifactVersion.class, new ArtifactVersionAdapter())
+ .registerTypeAdapter(VersionRange.class, new VersionRangeAdapter())
+ .registerTypeAdapter(InheritableMetadata.class, new InheritableMetadataAdapter())
+ .registerTypeAdapter(MetadataContainer.class, new MetadataContainerAdapter())
+ .registerTypeAdapter(StandardPluginMetadata.Builder.class, new StandardPluginMetadataBuilderDeserializer())
+ .registerTypeAdapter(StandardPluginMetadata.class, new StandardPluginMetadataSerializer())
+ .create();
+
+ private static final Set warnings = new LinkedHashSet<>();
private MetadataParser() {
}
- public static GsonBuilder gsonBuilder() {
- return new GsonBuilder()
- .registerTypeAdapter(MetadataContainer.class, new MetadataContainer.Serializer())
- .registerTypeAdapter(StandardInheritable.class, new StandardInheritable.Serializer())
- .registerTypeAdapter(StandardPluginMetadata.Builder.class, new StandardPluginMetadata.Deserializer())
- .registerTypeAdapter(StandardPluginMetadata.class, new StandardPluginMetadata.Serializer())
- ;
+ public static Gson gson() {
+ return MetadataParser.GSON;
+ }
+
+ /**
+ * Returns all warnings generated by (de)serializers so that the platform may log them when appropriate.
+ *
+ * @return The warnings
+ */
+ public static Set warnings() {
+ return Collections.unmodifiableSet(MetadataParser.warnings);
+ }
+
+ /**
+ * Adds a warning generated by a (de)serializer so that the platform may log it later.
+ *
+ * @param warning The warning
+ */
+ public static void addWarning(final String warning) {
+ MetadataParser.warnings.add(Objects.requireNonNull(warning));
}
/**
* Reads a {@link MetadataContainer container} from a given {@link Path path} using the default {@link Gson deserializer}
- * (retrieved from {@link #gsonBuilder()}).
+ * (retrieved from {@link #gson()}).
*
* @param path The path
* @return The container
* @throws IOException if the container fails to be read
*/
public static MetadataContainer read(final Path path) throws IOException {
- return MetadataParser.read(path, MetadataParser.gsonBuilder().create());
+ return MetadataParser.read(path, MetadataParser.gson());
}
/**
* Reads a {@link MetadataContainer container} from a given {@link Path path} with configured {@link Gson deserializer}.
*
- * To get a standard deserializer, {@link MetadataParser#gsonBuilder()} is available.
+ * To get a standard deserializer, {@link MetadataParser#gson()} is available.
* @param path The path
* @param gson The deserializer
* @return The container
@@ -83,20 +124,20 @@ public static MetadataContainer read(final Path path, final Gson gson) throws IO
/**
* Reads a {@link MetadataContainer container} from a given {@link Reader reader} using the default {@link Gson deserializer}
- * (retrieved from {@link #gsonBuilder()}).
+ * (retrieved from {@link #gson()}).
*
* @param reader The reader
* @return The container
* @throws IOException if the container fails to be read
*/
public static MetadataContainer read(final Reader reader) throws IOException {
- return MetadataParser.read(reader, MetadataParser.gsonBuilder().create());
+ return MetadataParser.read(reader, MetadataParser.gson());
}
/**
* Reads a {@link MetadataContainer container} from a given {@link Reader reader} with configured {@link Gson deserializer}.
*
- * To get a standard deserializer, {@link MetadataParser#gsonBuilder()} is available.
+ * To get a standard deserializer, {@link MetadataParser#gson()} is available.
* @param reader The reader
* @param gson The deserializer
* @return The container
@@ -111,10 +152,23 @@ public static MetadataContainer read(final Reader reader, final Gson gson) throw
}
}
+ /**
+ * Writes a {@link MetadataContainer container} to the given {@link Path path} using the default {@link Gson deserializer}
+ * (retrieved from {@link #gson()}).
+ *
+ * @param path The path
+ * @param container The container
+ * @param indent True to indent (pretty print) the resulting JSON, false if not
+ * @throws IOException If the container fails to serialize
+ */
+ public static void write(final Path path, final MetadataContainer container, final boolean indent) throws IOException {
+ MetadataParser.write(path, container, MetadataParser.gson(), indent);
+ }
+
/**
* Writes a {@link MetadataContainer container} to the given {@link Path path} using the configured {@link Gson serializer}.
*
- * To get a standard serializer, {@link MetadataParser#gsonBuilder()} is available.
+ * To get a standard serializer, {@link MetadataParser#gson()} is available.
* @param path The path
* @param container The container
* @param gson The serializer
@@ -134,10 +188,23 @@ public static void write(final Path path, final MetadataContainer container, fin
}
}
+ /**
+ * Writes a {@link MetadataContainer container} to the given {@link Path path} using the default {@link Gson deserializer}
+ * (retrieved from {@link #gson()}).
+ *
+ * @param writer The writer
+ * @param container The container
+ * @param indent True to indent (pretty print) the resulting JSON, false if not
+ * @throws IOException If the container fails to serialize
+ */
+ public static void write(final Writer writer, final MetadataContainer container, final boolean indent) throws IOException {
+ MetadataParser.write(writer, container, MetadataParser.gson(), indent);
+ }
+
/**
* Writes a {@link MetadataContainer container} to the given {@link Path path} using the configured {@link Gson serializer}.
*
- * To get a standard serializer, {@link MetadataParser#gsonBuilder()} is available.
+ * To get a standard serializer, {@link MetadataParser#gson()} is available.
* @param writer The writer
* @param container The container
* @param gson The serializer
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/MissingRequiredFieldException.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/MissingRequiredFieldException.java
index 02b8981..402e16f 100644
--- a/src/main/java/org/spongepowered/plugin/metadata/builtin/MissingRequiredFieldException.java
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/MissingRequiredFieldException.java
@@ -26,8 +26,11 @@
import com.google.gson.JsonParseException;
+import java.io.Serial;
+
public final class MissingRequiredFieldException extends JsonParseException {
+ @Serial
private static final long serialVersionUID = 223908766837278356L;
public MissingRequiredFieldException(final String field) {
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardInheritable.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardInheritable.java
deleted file mode 100644
index d542ab2..0000000
--- a/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardInheritable.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * This file is part of plugin-meta, licensed under the MIT License (MIT).
- *
- * Copyright (c) SpongePowered
- * Copyright (c) contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package org.spongepowered.plugin.metadata.builtin;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonPrimitive;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
-import org.apache.maven.artifact.versioning.ArtifactVersion;
-import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.spongepowered.plugin.metadata.Inheritable;
-import org.spongepowered.plugin.metadata.builtin.model.Adapters;
-import org.spongepowered.plugin.metadata.builtin.model.StandardPluginBranding;
-import org.spongepowered.plugin.metadata.builtin.model.StandardPluginContributor;
-import org.spongepowered.plugin.metadata.builtin.model.StandardPluginDependency;
-import org.spongepowered.plugin.metadata.builtin.model.StandardPluginLinks;
-import org.spongepowered.plugin.metadata.model.PluginContributor;
-import org.spongepowered.plugin.metadata.model.PluginDependency;
-import org.spongepowered.plugin.metadata.util.GsonUtils;
-
-import java.lang.reflect.Type;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.StringJoiner;
-import java.util.stream.Collectors;
-
-@SuppressWarnings("unchecked")
-public class StandardInheritable implements Inheritable {
-
- protected final ArtifactVersion version;
- protected final String rawVersion;
- protected final StandardPluginBranding branding;
- protected final StandardPluginLinks links;
- protected final List contributors = new LinkedList<>();
- protected final Set dependencies = new LinkedHashSet<>();
- protected final Map properties = new LinkedHashMap<>();
- private final Map dependenciesById = new LinkedHashMap<>();
-
- @SuppressWarnings("rawtypes")
- protected StandardInheritable(final AbstractBuilder builder) {
- this.version = builder.version;
- this.rawVersion = builder.rawVersion;
- this.branding = builder.branding;
- this.links = builder.links;
- this.contributors.addAll(builder.contributors);
- this.dependencies.addAll(builder.dependencies);
- for (final StandardPluginDependency dependency : this.dependencies) {
- this.dependenciesById.put(dependency.id(), dependency);
- }
- this.properties.putAll(builder.properties);
- }
-
- @Override
- public ArtifactVersion version() {
- return this.version;
- }
-
- @Override
- public StandardPluginBranding branding() {
- return this.branding;
- }
-
- @Override
- public StandardPluginLinks links() {
- return this.links;
- }
-
- @Override
- public List contributors() {
- return Collections.unmodifiableList(this.contributors);
- }
-
- @Override
- public Optional dependency(final String id) {
- return Optional.ofNullable(this.dependenciesById.get(Objects.requireNonNull(id, "id")));
- }
-
- @Override
- public Set dependencies() {
- return Collections.unmodifiableSet(this.dependencies);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public Optional property(final String key) {
- return Optional.ofNullable((T) this.properties.get(Objects.requireNonNull(key, "key")));
- }
-
- @Override
- public Map properties() {
- return Collections.unmodifiableMap(this.properties);
- }
-
- @Override
- public String toString() {
- return this.stringJoiner().toString();
- }
-
- protected StringJoiner stringJoiner() {
- return new StringJoiner(", ", StandardInheritable.class.getSimpleName() + "[", "]")
- .add("version=" + this.rawVersion)
- .add("branding=" + this.branding)
- .add("links=" + this.links)
- .add("contributors=" + this.contributors)
- .add("dependencies=" + this.dependencies)
- .add("properties=" + this.properties)
- ;
- }
-
- @SuppressWarnings("unchecked")
- public abstract static class AbstractBuilder> {
-
- final List contributors = new LinkedList<>();
- final Set dependencies = new LinkedHashSet<>();
- final Map properties = new LinkedHashMap<>();
- ArtifactVersion version = NullVersion.instance();
- String rawVersion = "null";
- StandardPluginBranding branding = StandardPluginBranding.none();
- StandardPluginLinks links = StandardPluginLinks.none();
-
- protected AbstractBuilder() {
- }
-
- public B version(final @Nullable String version) {
- if (version == null) {
- this.version = NullVersion.instance();
- this.rawVersion = "null";
- } else {
- this.version = new DefaultArtifactVersion(Objects.requireNonNull(version, "version"));
- this.rawVersion = version;
- }
- return (B) this;
- }
-
- public B branding(final StandardPluginBranding branding) {
- this.branding = Objects.requireNonNull(branding, "branding");
- return (B) this;
- }
-
- public B links(final StandardPluginLinks links) {
- this.links = Objects.requireNonNull(links, "links");
- return (B) this;
- }
-
- public B contributors(final Collection contributors) {
- this.contributors.addAll(Objects.requireNonNull(contributors, "contributors"));
- return (B) this;
- }
-
- public B addContributor(final StandardPluginContributor contributor) {
- this.contributors.add(Objects.requireNonNull(contributor, "contributor"));
- return (B) this;
- }
-
- public B dependencies(final Collection dependencies) {
- this.dependencies.addAll(Objects.requireNonNull(dependencies, "dependencies"));
- return (B) this;
- }
-
- public B addDependency(final StandardPluginDependency dependency) {
- this.dependencies.add(Objects.requireNonNull(dependency, "dependency"));
- return (B) this;
- }
-
- public B properties(final Map properties) {
- this.properties.putAll(Objects.requireNonNull(properties, "properties"));
- return (B) this;
- }
-
- public B property(final String key, final Object value) {
- this.properties.put(Objects.requireNonNull(key, "key"), Objects.requireNonNull(value, "value"));
- return (B) this;
- }
-
- public B from(final StandardInheritable value) {
- // TODO If we have "partial" entries, do we want to do deep merging? May start to get out of control...
-
- // Inheritable
- if (this.version == NullVersion.instance()) {
- this.version = value.version();
- this.rawVersion = value.rawVersion;
- }
- if (this.branding == StandardPluginBranding.none()) {
- this.branding = value.branding();
- }
- if (this.links == StandardPluginLinks.none()) {
- this.links = value.links;
- }
- this.contributors.addAll(value.contributors);
- this.dependencies.addAll(value.dependencies);
- for (final Map.Entry entry : value.properties().entrySet()) {
- this.properties.putIfAbsent(entry.getKey(), entry.getValue());
- }
- return (B) this;
- }
-
- public final T build() {
- Objects.requireNonNull(this.version, "version");
-
- return this.build0();
- }
-
- protected abstract T build0();
- }
-
- public static final class Builder extends AbstractBuilder {
-
- @Override
- protected StandardInheritable build0() {
- return new StandardInheritable(this);
- }
- }
-
- public static final class Serializer implements JsonSerializer, JsonDeserializer {
-
- @Override
- public StandardInheritable deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context)
- throws JsonParseException {
-
- final JsonObject obj = element.getAsJsonObject();
- final Builder builder = new Builder();
- builder.version(GsonUtils.get(obj, "version", JsonElement::getAsString).orElse(null));
- GsonUtils.consumeIfPresent(obj, "branding", e -> builder.branding(Adapters.Deserializers.PLUGIN_BRANDING.fromJsonTree(e).build()));
- GsonUtils.consumeIfPresent(obj, "links", e -> builder.links(Adapters.Deserializers.PLUGIN_LINKS.fromJsonTree(e).build()));
- GsonUtils.consumeIfPresent(obj, "contributors", e -> builder.contributors(GsonUtils.read((JsonArray) e, Adapters.Deserializers.PLUGIN_CONTRIBUTOR, LinkedList::new).stream().map(StandardPluginContributor.Builder::build).collect(Collectors.toList())));
- GsonUtils.consumeIfPresent(obj, "dependencies", e -> builder.dependencies(GsonUtils.read((JsonArray) e, Adapters.Deserializers.PLUGIN_DEPENDENCY, LinkedHashSet::new).stream().map(StandardPluginDependency.Builder::build).collect(Collectors.toList())));
- GsonUtils.consumeIfPresent(obj, "properties", e -> builder.properties(GsonUtils.read((JsonObject) e, JsonElement::getAsString, LinkedHashMap::new)));
-
- return builder.build();
- }
-
- @Override
- public JsonElement serialize(final StandardInheritable value, final Type type, final JsonSerializationContext context) {
- final JsonObject obj = new JsonObject();
- GsonUtils.applyIfValid(obj, value, p -> p.version != NullVersion.instance(), (o, v) -> o.addProperty("version", v.rawVersion));
- GsonUtils.applyIfValid(obj, value, p -> p.branding != StandardPluginBranding.none(), (o, v) -> o.add("branding", Adapters.Serializers.PLUGIN_BRANDING.toJsonTree(v.branding)));
- GsonUtils.applyIfValid(obj, value, p -> p.links != StandardPluginLinks.none(), (o, v) -> o.add("links", Adapters.Serializers.PLUGIN_LINKS.toJsonTree(v.links)));
- GsonUtils.applyIfValid(obj, value, p -> !p.contributors.isEmpty(), (o, v) -> o.add("contributors", GsonUtils.write(Adapters.Serializers.PLUGIN_CONTRIBUTOR, v.contributors)));
- GsonUtils.applyIfValid(obj, value, p -> !p.dependencies.isEmpty(), (o, v) -> o.add("dependencies", GsonUtils.write(Adapters.Serializers.PLUGIN_DEPENDENCY, v.dependencies)));
- GsonUtils.applyIfValid(obj, value, p -> !p.properties.isEmpty(), (o, v) -> o.add("properties", GsonUtils.write(e -> new JsonPrimitive(e.toString()), v.properties)));
- return obj;
- }
- }
-}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java
index ad0f8d9..679f3dd 100644
--- a/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/StandardPluginMetadata.java
@@ -24,56 +24,86 @@
*/
package org.spongepowered.plugin.metadata.builtin;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.plugin.metadata.Constants;
-import org.spongepowered.plugin.metadata.Container;
import org.spongepowered.plugin.metadata.PluginMetadata;
-import org.spongepowered.plugin.metadata.util.GsonUtils;
-
-import java.lang.reflect.Type;
+import org.spongepowered.plugin.metadata.model.PluginBranding;
+import org.spongepowered.plugin.metadata.model.PluginContributor;
+import org.spongepowered.plugin.metadata.model.PluginDependency;
+import org.spongepowered.plugin.metadata.model.PluginEntrypoints;
+import org.spongepowered.plugin.metadata.model.PluginLinks;
+import org.spongepowered.plugin.metadata.model.PluginLoaderSpecification;
+
+import java.util.Collection;
+import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
-public final class StandardPluginMetadata extends StandardInheritable implements PluginMetadata {
-
- private final String id, entrypoint;
- @Nullable private final String name, description;
- @Nullable private Container container;
+public final class StandardPluginMetadata implements PluginMetadata {
+ private final String id;
+ private final PluginEntrypoints entrypoints;
+ private final InheritableMetadata global, override;
+
+ private final ArtifactVersion version;
+ private final PluginLoaderSpecification loader;
+ private final @Nullable String name;
+ private final @Nullable String description;
+ private final @Nullable String license;
+ private final PluginBranding branding;
+ private final PluginLinks links;
+ private final List contributors;
+ private final Map dependencies;
+ private final Map properties;
private StandardPluginMetadata(final Builder builder) {
- super(builder);
this.id = builder.id;
- this.entrypoint = builder.entrypoint;
- this.name = builder.name;
- this.description = builder.description;
+ this.entrypoints = builder.entrypoints;
+ this.global = builder.global;
+ this.override = builder.override;
+ InheritableMetadata metadata = this.global.with(this.override);
+ this.version = metadata.version().orElseThrow(() -> new NoSuchElementException("version"));
+ this.loader = metadata.loader().orElseThrow(() -> new NoSuchElementException("loader"));
+ this.name = metadata.name().orElse(null);
+ this.description = metadata.description().orElse(null);
+ this.license = metadata.license().orElse(null);
+ this.branding = metadata.branding();
+ this.links = metadata.links();
+ this.contributors = metadata.contributors();
+ this.dependencies = metadata.dependencies();
+ this.properties = metadata.properties();
}
- public static StandardPluginMetadata.Builder builder() {
- return new Builder();
+ @Override
+ public String id() {
+ return this.id;
}
@Override
- public Container container() {
- return this.container;
+ public PluginEntrypoints entrypoints() {
+ return this.entrypoints;
+ }
+
+ public InheritableMetadata global() {
+ return this.global;
+ }
+
+ public InheritableMetadata override() {
+ return this.override;
}
@Override
- public String id() {
- return this.id;
+ public ArtifactVersion version() {
+ return this.version;
}
@Override
- public String entrypoint() {
- return this.entrypoint;
+ public PluginLoaderSpecification loader() {
+ return this.loader;
}
@Override
@@ -86,13 +116,50 @@ public Optional description() {
return Optional.ofNullable(this.description);
}
- void setContainer(final MetadataContainer container) {
- this.container = container;
+ @Override
+ public Optional license() {
+ return Optional.ofNullable(this.license);
+ }
+
+ @Override
+ public PluginBranding branding() {
+ return this.branding;
+ }
+
+ @Override
+ public PluginLinks links() {
+ return this.links;
+ }
+
+ @Override
+ public List contributors() {
+ return this.contributors;
+ }
+
+ @Override
+ public Optional dependency(String id) {
+ return Optional.ofNullable(this.dependencies.get(Objects.requireNonNull(id, "id")));
+ }
+
+ @Override
+ public Collection dependencies() {
+ return this.dependencies.values();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Optional property(final String key) {
+ return Optional.ofNullable((T) this.properties.get(Objects.requireNonNull(key, "key")));
+ }
+
+ @Override
+ public Map properties() {
+ return this.properties;
}
@Override
public int hashCode() {
- return Objects.hashCode(this.id);
+ return Objects.hash(this.id, this.entrypoints, this.global, this.override);
}
@Override
@@ -101,107 +168,80 @@ public boolean equals(final Object o) {
return true;
}
- if (!(o instanceof StandardPluginMetadata)) {
+ if (!(o instanceof StandardPluginMetadata other)) {
return false;
}
- final StandardPluginMetadata other = (StandardPluginMetadata) o;
- return this.id.equals(other.id);
+ return this.id.equals(other.id) && this.entrypoints.equals(other.entrypoints)
+ && this.global.equals(other.global) && this.override.equals(other.override);
}
@Override
public String toString() {
- final StringJoiner joiner = new StringJoiner(", ", StandardPluginMetadata.class.getSimpleName() + "[", "]")
+ return new StringJoiner(", ", StandardPluginMetadata.class.getSimpleName() + "[", "]")
.add("id=" + this.id)
- .add("name=" + this.name)
- .add("entrypoint=" + this.entrypoint)
- .add("description=" + this.description);
- joiner.merge(this.stringJoiner());
- return joiner.toString();
+ .add("entrypoints=" + this.entrypoints)
+ .add("version=" + this.version)
+ .add("loader=" + this.loader)
+ .add("license=" + this.license)
+ .add("branding=" + this.branding)
+ .add("links=" + this.links)
+ .add("contributors=" + this.contributors)
+ .add("dependencies=" + this.dependencies)
+ .add("properties=" + this.properties)
+ .toString();
+ }
+
+ public StandardPluginMetadata.Builder toBuilder() {
+ return new StandardPluginMetadata.Builder().from(this);
}
- public static final class Builder extends StandardInheritable.AbstractBuilder {
+ public static StandardPluginMetadata.Builder builder() {
+ return new StandardPluginMetadata.Builder();
+ }
- @Nullable String id, entrypoint, name, description;
+ public static final class Builder {
- public Builder() {
- }
+ private @MonotonicNonNull String id;
+ private PluginEntrypoints entrypoints = PluginEntrypoints.none();
+ private InheritableMetadata global = InheritableMetadata.none(), override = InheritableMetadata.none();
+
+ private Builder() {}
public Builder id(final String id) {
this.id = Objects.requireNonNull(id, "id");
return this;
}
- public Builder entrypoint(final String entrypoint) {
- this.entrypoint = Objects.requireNonNull(entrypoint, "entrypoint");
+ public Builder entrypoints(final PluginEntrypoints entrypoints) {
+ this.entrypoints = Objects.requireNonNull(entrypoints, "entrypoints");
+ return this;
+ }
+
+ public Builder global(final InheritableMetadata global) {
+ this.global = Objects.requireNonNull(global, "global");
return this;
}
- public Builder name(final String name) {
- this.name = Objects.requireNonNull(name, "name");
+ public Builder override(final InheritableMetadata override) {
+ this.override = Objects.requireNonNull(override, "override");
return this;
}
- public Builder description(final String description) {
- this.description = Objects.requireNonNull(description, "description");
+ public Builder from(final StandardPluginMetadata value) {
+ this.id = value.id;
+ this.entrypoints = value.entrypoints;
+ this.global = value.global;
+ this.override = value.override;
return this;
}
- @Override
- protected StandardPluginMetadata build0() {
+ public StandardPluginMetadata build() {
if (!Constants.VALID_ID_PATTERN.matcher(Objects.requireNonNull(this.id, "id")).matches()) {
throw new IllegalStateException(String.format("PluginMetadata with supplied ID '{%s}' is invalid. %s", this.id,
Constants.INVALID_ID_REQUIREMENTS_MESSAGE));
}
- if (this.version == NullVersion.instance()) {
- throw new IllegalStateException(String.format("PluginMetadata with supplied ID '{%s}' has no version specified.", this.id));
- }
- Objects.requireNonNull(this.entrypoint, "entrypoint");
-
return new StandardPluginMetadata(this);
}
}
-
- public static final class Deserializer implements JsonDeserializer {
-
- @Override
- public StandardPluginMetadata.Builder deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context)
- throws JsonParseException {
- final JsonObject obj = element.getAsJsonObject();
- if (!obj.has("id")) {
- throw new MissingRequiredFieldException("id");
- }
- if (!obj.has("entrypoint")) {
- throw new MissingRequiredFieldException("entrypoint");
- }
- final StandardPluginMetadata.Builder builder = new StandardPluginMetadata.Builder();
- builder
- .id(obj.get("id").getAsString())
- .entrypoint(obj.get("entrypoint").getAsString());
- GsonUtils.consumeIfPresent(obj, "name", e -> builder.name(e.getAsString()));
- GsonUtils.consumeIfPresent(obj, "description", e -> builder.description(e.getAsString()));
- builder.from(context.deserialize(element, StandardInheritable.class));
-
- return builder;
- }
- }
-
- public static final class Serializer implements JsonSerializer {
-
- @Override
- public JsonElement serialize(final StandardPluginMetadata value, final Type type, final JsonSerializationContext context) {
- final JsonObject obj = new JsonObject();
- obj.addProperty("id", value.id);
- obj.addProperty("entrypoint", value.entrypoint);
- GsonUtils.writeIfPresent(obj, "name", value.name());
- GsonUtils.writeIfPresent(obj, "description", value.description());
-
- final JsonObject inheritableElement = (JsonObject) context.serialize(value, StandardInheritable.class);
- // TODO Sort this?
- for (final Map.Entry entry : inheritableElement.entrySet()) {
- obj.add(entry.getKey(), entry.getValue());
- }
- return obj;
- }
- }
}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java
new file mode 100644
index 0000000..ddd4c9e
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/InheritableMetadataAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter;
+
+import com.google.gson.*;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.spongepowered.plugin.metadata.builtin.InheritableMetadata;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+import org.spongepowered.plugin.metadata.model.PluginBranding;
+import org.spongepowered.plugin.metadata.model.PluginContributor;
+import org.spongepowered.plugin.metadata.model.PluginDependency;
+import org.spongepowered.plugin.metadata.model.PluginLinks;
+import org.spongepowered.plugin.metadata.model.PluginLoaderSpecification;
+
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+
+public final class InheritableMetadataAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public InheritableMetadata deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return InheritableMetadata.builder()
+ .version(GsonUtils.optional(obj, "version").map(v -> context.deserialize(v, ArtifactVersion.class)).orElse(null))
+ .loader(GsonUtils.optional(obj, "loader").map(v -> context.deserialize(v, PluginLoaderSpecification.class)).orElse(null))
+ .name(GsonUtils.optional(obj, "name").map(JsonElement::getAsString).orElse(null))
+ .description(GsonUtils.optional(obj, "description").map(JsonElement::getAsString).orElse(null))
+ .license(GsonUtils.optional(obj, "license").map(JsonElement::getAsString).orElse(null))
+ .branding(GsonUtils.optional(obj, "branding").map(v -> context.deserialize(v, PluginBranding.class)).orElseGet(PluginBranding::none))
+ .links(GsonUtils.optional(obj, "links").map(v -> context.deserialize(v, PluginLinks.class)).orElseGet(PluginLinks::none))
+ .contributors(GsonUtils.stream(obj, "contributors").map(v -> context.deserialize(v, PluginContributor.class)).toList())
+ .dependencies(GsonUtils.stream(obj, "dependencies").map(v -> context.deserialize(v, PluginDependency.class)).toList())
+ .properties(GsonUtils.deserializeMap(obj.get("properties"), JsonElement::getAsString, LinkedHashMap::new))
+ .build();
+ }
+
+ @Override
+ public JsonElement serialize(final InheritableMetadata value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ value.version().ifPresent(v -> obj.add("version", context.serialize(v, ArtifactVersion.class)));
+ value.loader().ifPresent(v -> obj.add("loader", context.serialize(v, PluginLoaderSpecification.class)));
+ value.name().ifPresent(v -> obj.addProperty("name", v));
+ value.description().ifPresent(v -> obj.addProperty("description", v));
+ value.license().ifPresent(v -> obj.addProperty("license", v));
+ if (!value.branding().equals(PluginBranding.none())) {
+ obj.add("branding", context.serialize(value.branding(), PluginBranding.class));
+ }
+ if (!value.links().equals(PluginLinks.none())) {
+ obj.add("links", context.serialize(value.links(), PluginLinks.class));
+ }
+ if (!value.contributors().isEmpty()) {
+ obj.add("contributors", GsonUtils.toArray(value.contributors().stream().map(v -> context.serialize(v, PluginContributor.class))));
+ }
+ if (!value.dependencies().isEmpty()) {
+ obj.add("dependencies", GsonUtils.toArray(value.dependencies().values().stream().map(v -> context.serialize(v, PluginDependency.class))));
+ }
+ if (!value.properties().isEmpty()) {
+ obj.add("properties", GsonUtils.serializeMap(value.properties(), v -> new JsonPrimitive(v.toString())));
+ }
+ return obj;
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/MetadataContainerAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/MetadataContainerAdapter.java
new file mode 100644
index 0000000..db284ab
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/MetadataContainerAdapter.java
@@ -0,0 +1,90 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.spongepowered.plugin.metadata.builtin.MetadataContainer;
+import org.spongepowered.plugin.metadata.builtin.InheritableMetadata;
+import org.spongepowered.plugin.metadata.builtin.StandardPluginMetadata;
+import org.spongepowered.plugin.metadata.model.PluginLoaderSpecification;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+
+import java.lang.reflect.Type;
+import java.util.LinkedList;
+import java.util.List;
+
+public final class MetadataContainerAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public MetadataContainer deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+
+ // Read some global data in the root element for retro-compatibility
+ InheritableMetadata global = InheritableMetadata.builder()
+ .loader(GsonUtils.optional(obj, "loader").map(v -> context.deserialize(v, PluginLoaderSpecification.class)).orElse(null))
+ .license(GsonUtils.optional(obj, "license").map(JsonElement::getAsString).orElse(null))
+ .build();
+
+ final JsonElement globalElement = obj.get("global");
+ if (globalElement instanceof JsonObject) {
+ global = global.with(context.deserialize(globalElement, InheritableMetadata.class));
+ }
+
+ final List plugins = new LinkedList<>();
+ if (GsonUtils.require(obj, "plugins") instanceof JsonArray pluginsArray) {
+ for (final JsonElement pluginElement : pluginsArray) {
+ if (pluginElement.isJsonObject()) {
+ plugins.add(context.deserialize(pluginElement, StandardPluginMetadata.Builder.class)
+ .global(global).build());
+ }
+ }
+ }
+
+ return new MetadataContainer(global, plugins);
+ }
+
+ @Override
+ public JsonElement serialize(final MetadataContainer value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ if (!value.global().equals(InheritableMetadata.none())) {
+ obj.add("global", context.serialize(value.global(), InheritableMetadata.class));
+ }
+
+ final JsonArray plugins = new JsonArray();
+ for (final StandardPluginMetadata plugin : value.plugins()) {
+ plugins.add(context.serialize(plugin, StandardPluginMetadata.class));
+ }
+ obj.add("plugins", plugins);
+
+ return obj;
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/StandardPluginMetadataBuilderDeserializer.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/StandardPluginMetadataBuilderDeserializer.java
new file mode 100644
index 0000000..fe2c132
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/StandardPluginMetadataBuilderDeserializer.java
@@ -0,0 +1,64 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter;
+
+import com.google.gson.*;
+import org.spongepowered.plugin.metadata.builtin.InheritableMetadata;
+import org.spongepowered.plugin.metadata.builtin.StandardPluginMetadata;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.LegacyIds;
+import org.spongepowered.plugin.metadata.model.PluginEntrypoints;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+public final class StandardPluginMetadataBuilderDeserializer implements JsonDeserializer {
+
+ @Override
+ public StandardPluginMetadata.Builder deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return StandardPluginMetadata.builder()
+ .id(LegacyIds.fix(GsonUtils.require(obj, "id").getAsString()))
+ .entrypoints(
+ GsonUtils.optional(obj, "entrypoints").map(v -> StandardPluginMetadataBuilderDeserializer.deserializeEntrypoints(v, context))
+ .or(() -> GsonUtils.optional(obj, "entrypoint").map(v -> new PluginEntrypoints(List.of(v.getAsString())))) // legacy
+ .orElseGet(PluginEntrypoints::none)
+ )
+ .override(context.deserialize(element, InheritableMetadata.class));
+ }
+
+ /**
+ * Should be in PluginEntrypointsAdapter but Gson refuses to pass a JsonArray to a JsonDeserializer.
+ */
+ private static PluginEntrypoints deserializeEntrypoints(final JsonElement element, final JsonDeserializationContext context) {
+ if (element.isJsonNull()) {
+ return PluginEntrypoints.none();
+ }
+ if (element instanceof JsonArray array) {
+ return new PluginEntrypoints(array.asList().stream().map(JsonElement::getAsString).toList());
+ }
+ return context.deserialize(element, PluginEntrypoints.class);
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/StandardPluginMetadataSerializer.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/StandardPluginMetadataSerializer.java
new file mode 100644
index 0000000..5cc32ee
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/StandardPluginMetadataSerializer.java
@@ -0,0 +1,63 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.spongepowered.plugin.metadata.builtin.InheritableMetadata;
+import org.spongepowered.plugin.metadata.builtin.StandardPluginMetadata;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+import org.spongepowered.plugin.metadata.model.PluginEntrypoints;
+
+import java.lang.reflect.Type;
+import java.util.Optional;
+
+public final class StandardPluginMetadataSerializer implements JsonSerializer {
+
+ @Override
+ public JsonElement serialize(final StandardPluginMetadata value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("id", value.id());
+ StandardPluginMetadataSerializer.serializeEntrypoints(value.entrypoints(), context).ifPresent(v -> obj.add("entrypoints", v));
+ obj.asMap().putAll(((JsonObject) context.serialize(value.override(), InheritableMetadata.class)).asMap());
+ return obj;
+ }
+
+ /**
+ * Should be in PluginEntrypointsAdapter but Gson refuses to pass a JsonArray to a JsonDeserializer.
+ */
+ private static Optional serializeEntrypoints(final PluginEntrypoints value, final JsonSerializationContext context) {
+ if (value.server().isEmpty() && value.client().isEmpty()) {
+ if (value.main().isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(GsonUtils.toArray(value.main().stream().map(JsonPrimitive::new)));
+ }
+ return Optional.of(context.serialize(value, PluginEntrypoints.class));
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginBrandingAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginBrandingAdapter.java
new file mode 100644
index 0000000..0e22b70
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginBrandingAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.model;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.spongepowered.plugin.metadata.model.PluginBranding;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+
+import java.lang.reflect.Type;
+
+public final class PluginBrandingAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public PluginBranding deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return new PluginBranding(
+ GsonUtils.optional(obj, "logo").map(JsonElement::getAsString),
+ GsonUtils.optional(obj, "icon").map(JsonElement::getAsString)
+ );
+ }
+
+ @Override
+ public JsonElement serialize(final PluginBranding value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ value.logo().ifPresent(v -> obj.addProperty("logo", v));
+ value.icon().ifPresent(v -> obj.addProperty("icon", v));
+ return obj;
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginContributorAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginContributorAdapter.java
new file mode 100644
index 0000000..8b89a36
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginContributorAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.model;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.spongepowered.plugin.metadata.model.PluginContributor;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+
+import java.lang.reflect.Type;
+
+public final class PluginContributorAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public PluginContributor deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return new PluginContributor(
+ GsonUtils.require(obj, "name").getAsString(),
+ GsonUtils.optional(obj, "description").map(JsonElement::getAsString)
+ );
+ }
+
+ @Override
+ public JsonElement serialize(final PluginContributor value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("name", value.name());
+ value.description().ifPresent(v -> obj.addProperty("description", v));
+ return obj;
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginDependencyAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginDependencyAdapter.java
new file mode 100644
index 0000000..97917f5
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginDependencyAdapter.java
@@ -0,0 +1,59 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.model;
+
+import com.google.gson.*;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.LegacyIds;
+import org.spongepowered.plugin.metadata.model.PluginDependency;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+
+import java.lang.reflect.Type;
+
+public final class PluginDependencyAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public PluginDependency deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return new PluginDependency(
+ LegacyIds.fix(GsonUtils.require(obj, "id").getAsString()),
+ context.deserialize(GsonUtils.require(obj, "version"), VersionRange.class),
+ GsonUtils.optional(obj, "load-order")
+ .map(v -> context.deserialize(v, PluginDependency.LoadOrder.class))
+ .orElse(PluginDependency.LoadOrder.UNDEFINED),
+ GsonUtils.optional(obj, "optional").map(JsonElement::getAsBoolean).orElse(false)
+ );
+ }
+
+ @Override
+ public JsonElement serialize(final PluginDependency value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("id", value.id());
+ obj.add("version", context.serialize(value.version(), VersionRange.class));
+ obj.add("load-order", context.serialize(value.loadOrder(), PluginDependency.LoadOrder.class));
+ obj.addProperty("optional", value.optional());
+ return obj;
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginEntrypointsAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginEntrypointsAdapter.java
new file mode 100644
index 0000000..b425efe
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginEntrypointsAdapter.java
@@ -0,0 +1,64 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.model;
+
+import com.google.gson.*;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+import org.spongepowered.plugin.metadata.model.PluginEntrypoints;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+public final class PluginEntrypointsAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public PluginEntrypoints deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return new PluginEntrypoints(
+ PluginEntrypointsAdapter.deserialize(obj, "main"),
+ PluginEntrypointsAdapter.deserialize(obj, "server"),
+ PluginEntrypointsAdapter.deserialize(obj, "client")
+ );
+ }
+
+ @Override
+ public JsonElement serialize(final PluginEntrypoints value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ PluginEntrypointsAdapter.serialize(obj, "main", value.main());
+ PluginEntrypointsAdapter.serialize(obj, "server", value.server());
+ PluginEntrypointsAdapter.serialize(obj, "client", value.client());
+ return obj;
+ }
+
+ private static List deserialize(final JsonObject obj, final String name) {
+ return GsonUtils.stream(obj, name).map(JsonElement::getAsString).toList();
+ }
+
+ private static void serialize(final JsonObject obj, final String name, final List list) {
+ if (!list.isEmpty()) {
+ obj.add(name, GsonUtils.toArray(list.stream().map(JsonPrimitive::new)));
+ }
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginLinksAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginLinksAdapter.java
new file mode 100644
index 0000000..d3725a0
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginLinksAdapter.java
@@ -0,0 +1,63 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.model;
+
+import com.google.gson.*;
+import org.spongepowered.plugin.metadata.model.PluginLinks;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.Optional;
+
+public final class PluginLinksAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public PluginLinks deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return new PluginLinks(
+ PluginLinksAdapter.deserialize(obj, "homepage", context),
+ PluginLinksAdapter.deserialize(obj, "source", context),
+ PluginLinksAdapter.deserialize(obj, "issues", context)
+ );
+ }
+
+ @Override
+ public JsonElement serialize(final PluginLinks value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ PluginLinksAdapter.serialize(obj, "homepage", context, value.homepage());
+ PluginLinksAdapter.serialize(obj, "source", context, value.source());
+ PluginLinksAdapter.serialize(obj, "issues", context, value.issues());
+ return obj;
+ }
+
+ private static Optional deserialize(final JsonObject obj, final String name, final JsonDeserializationContext context) throws JsonParseException {
+ return GsonUtils.optional(obj, name).map(v -> context.deserialize(v, URI.class));
+ }
+
+ private static void serialize(final JsonObject obj, final String name, final JsonSerializationContext context, final Optional value) throws JsonParseException {
+ value.ifPresent(v -> obj.add(name, context.serialize(v, URI.class)));
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginLoaderSpecificationAdapter.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginLoaderSpecificationAdapter.java
new file mode 100644
index 0000000..2a1f752
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/model/PluginLoaderSpecificationAdapter.java
@@ -0,0 +1,58 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.model;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.spongepowered.plugin.metadata.model.PluginLoaderSpecification;
+import org.spongepowered.plugin.metadata.builtin.adapter.util.GsonUtils;
+
+import java.lang.reflect.Type;
+
+public final class PluginLoaderSpecificationAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public PluginLoaderSpecification deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) throws JsonParseException {
+ final JsonObject obj = element.getAsJsonObject();
+ return new PluginLoaderSpecification(
+ GsonUtils.require(obj, "name").getAsString(),
+ context.deserialize(GsonUtils.require(obj, "version"), VersionRange.class)
+ );
+ }
+
+ @Override
+ public JsonElement serialize(final PluginLoaderSpecification value, final Type type, final JsonSerializationContext context) {
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("name", value.name());
+ obj.add("version", context.serialize(value.version(), VersionRange.class));
+ return obj;
+ }
+}
diff --git a/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/util/GsonUtils.java b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/util/GsonUtils.java
new file mode 100644
index 0000000..dd08293
--- /dev/null
+++ b/src/main/java/org/spongepowered/plugin/metadata/builtin/adapter/util/GsonUtils.java
@@ -0,0 +1,89 @@
+/*
+ * This file is part of plugin-meta, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) SpongePowered
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.spongepowered.plugin.metadata.builtin.adapter.util;
+
+import com.google.gson.*;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.plugin.metadata.builtin.MissingRequiredFieldException;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+/**
+ * This is not public API as this package is not exported by the module.
+ */
+public final class GsonUtils {
+
+ public static JsonElement require(final JsonObject obj, final String name) {
+ final JsonElement element = obj.get(name);
+ if (element == null || element.isJsonNull()) {
+ throw new MissingRequiredFieldException(name);
+ }
+ return element;
+ }
+
+ public static Optional optional(final JsonObject obj, final String name) {
+ final JsonElement element = obj.get(name);
+ if (element == null || element.isJsonNull()) {
+ return Optional.empty();
+ }
+ return Optional.of(element);
+ }
+
+ public static Stream stream(final JsonObject obj, final String name) {
+ final JsonElement element = obj.get(name);
+ if (element == null || element.isJsonNull()) {
+ return Stream.empty();
+ }
+ return element.getAsJsonArray().asList().stream();
+ }
+
+ public static JsonArray toArray(final Stream stream) {
+ final JsonArray array = new JsonArray();
+ stream.forEach(array::add);
+ return array;
+ }
+
+ public static Map deserializeMap(final @Nullable JsonElement element, final Function valFunc, final Supplier