Added javadocs & fixed API version & more

This commit is contained in:
ImDaBigBoss 2022-01-12 13:50:54 +01:00
parent 6757437193
commit 805f7f666a
10 changed files with 214 additions and 154 deletions

View file

@ -1,51 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* 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.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.extension;
import org.geysermc.api.GeyserApiBase;
import java.io.File;
import java.io.InputStream;
public interface Extension {
void onLoad();
void onEnable();
void onDisable();
boolean isEnabled();
boolean isDisabled();
File dataFolder();
ExtensionDescription description();
String name();
InputStream getResource(String filename);
void saveResource(String filename, boolean replace);
ClassLoader classLoader();
ExtensionLoader extensionLoader();
ExtensionLogger logger();
GeyserApiBase geyserApi();
}

View file

@ -25,12 +25,41 @@
package org.geysermc.geyser.api.extension; package org.geysermc.geyser.api.extension;
import java.util.*; import java.util.List;
public interface ExtensionDescription { public interface ExtensionDescription {
/**
* Gets the extension's name
*
* @return the extension's name
*/
String name(); String name();
/**
* Gets the extension's main class
*
* @return the extension's main class
*/
String main(); String main();
List<String> ApiVersions();
/**
* Gets the extension's api version
*
* @return the extension's api version
*/
String ApiVersion();
/**
* Gets the extension's description
*
* @return the extension's description
*/
String version(); String version();
/**
* Gets the extension's authors
*
* @return the extension's authors
*/
List<String> authors(); List<String> authors();
} }

View file

@ -27,15 +27,47 @@ package org.geysermc.geyser.api.extension;
import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
import java.io.File; import java.io.File;
import java.util.regex.Pattern;
public interface ExtensionLoader { public interface ExtensionLoader {
/**
* Loads an extension from a given file
*
* @param file the file to load the extension from
* @return the loaded extension
* @throws InvalidExtensionException
*/
GeyserExtension loadExtension(File file) throws InvalidExtensionException; GeyserExtension loadExtension(File file) throws InvalidExtensionException;
/**
* Gets an extension's description from a given file
*
* @param file the file to get the description from
* @return the extension's description
* @throws InvalidDescriptionException
*/
ExtensionDescription extensionDescription(File file) throws InvalidDescriptionException; ExtensionDescription extensionDescription(File file) throws InvalidDescriptionException;
Pattern[] extensionFilters();
/**
* Gets a class by its name from the extension's classloader
*
* @param name the name of the class
* @return the class
* @throws ClassNotFoundException
*/
Class<?> classByName(final String name) throws ClassNotFoundException; Class<?> classByName(final String name) throws ClassNotFoundException;
void enableExtension(Extension extension);
void disableExtension(Extension extension); /**
* Enables an extension
*
* @param extension the extension to enable
*/
void enableExtension(GeyserExtension extension);
/**
* Disables an extension
*
* @param extension the extension to disable
*/
void disableExtension(GeyserExtension extension);
} }

View file

@ -28,6 +28,7 @@ package org.geysermc.geyser.api.extension;
public interface ExtensionLogger { public interface ExtensionLogger {
/** /**
* Get the logger prefix * Get the logger prefix
*
* @return the logger prefix * @return the logger prefix
*/ */
String prefix(); String prefix();

View file

@ -30,34 +30,52 @@ import java.io.*;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
public class GeyserExtension implements Extension { public class GeyserExtension {
private boolean initialized = false; private boolean initialized = false;
private boolean enabled = false; private boolean enabled = false;
private File file = null; private File file = null;
private File dataFolder = null; private File dataFolder = null;
private ClassLoader classLoader = null; private ClassLoader classLoader = null;
private ExtensionLoader loader; private ExtensionLoader loader = null;
private ExtensionLogger logger; private ExtensionLogger logger = null;
private ExtensionDescription description = null; private ExtensionDescription description = null;
private GeyserApiBase api = null; private GeyserApiBase api = null;
@Override /**
* Called when the extension is loaded
*/
public void onLoad() { public void onLoad() {
} }
@Override /**
* Called when the extension is enabled
*/
public void onEnable() { public void onEnable() {
} }
@Override /**
* Called when the extension is disabled
*/
public void onDisable() { public void onDisable() {
} }
@Override /**
* Gets if the extension is enabled
*
* @return true if the extension is enabled
*/
public boolean isEnabled() { public boolean isEnabled() {
return this.enabled; return this.enabled;
} }
/**
* Gets if the extension is enabled
*
* @return true if the extension is enabled
*/
public void setEnabled(boolean value) { public void setEnabled(boolean value) {
if (this.enabled != value) { if (this.enabled != value) {
this.enabled = value; this.enabled = value;
@ -69,27 +87,34 @@ public class GeyserExtension implements Extension {
} }
} }
@Override /**
public boolean isDisabled() { * Gets the extension's data folder
return !this.enabled; *
} * @return the extension's data folder
*/
@Override
public File dataFolder() { public File dataFolder() {
return this.dataFolder; return this.dataFolder;
} }
@Override /**
* Gets the extension's description
*
* @return the extension's description
*/
public ExtensionDescription description() { public ExtensionDescription description() {
return this.description; return this.description;
} }
@Override /**
* Gets the extension's name
*
* @return the extension's name
*/
public String name() { public String name() {
return this.description.name(); return this.description.name();
} }
public void init(GeyserApiBase api, ExtensionLogger logger, ExtensionLoader loader, ExtensionDescription description, File dataFolder, File file) { public void init(GeyserApiBase api, ExtensionLoader loader, ExtensionLogger logger, ExtensionDescription description, File dataFolder, File file) {
if (!this.initialized) { if (!this.initialized) {
this.initialized = true; this.initialized = true;
this.file = file; this.file = file;
@ -102,7 +127,12 @@ public class GeyserExtension implements Extension {
} }
} }
@Override /**
* Gets a resource from the extension jar file
*
* @param filename the file name
* @return the input stream
*/
public InputStream getResource(String filename) { public InputStream getResource(String filename) {
if (filename == null) { if (filename == null) {
throw new IllegalArgumentException("Filename cannot be null"); throw new IllegalArgumentException("Filename cannot be null");
@ -123,7 +153,12 @@ public class GeyserExtension implements Extension {
} }
} }
@Override /**
* Saves a resource from the extension jar file to the extension's data folder
*
* @param filename the file name
* @param replace whether to replace the file if it already exists
*/
public void saveResource(String filename, boolean replace) { public void saveResource(String filename, boolean replace) {
if (filename == null || filename.equals("")) { if (filename == null || filename.equals("")) {
throw new IllegalArgumentException("ResourcePath cannot be null or empty"); throw new IllegalArgumentException("ResourcePath cannot be null or empty");
@ -161,22 +196,38 @@ public class GeyserExtension implements Extension {
} }
} }
@Override /**
* Gets the extension's class loader
*
* @return the extension's class loader
*/
public ClassLoader classLoader() { public ClassLoader classLoader() {
return this.classLoader; return this.classLoader;
} }
@Override /**
* Gets the extension's loader
*
* @return the extension's loader
*/
public ExtensionLoader extensionLoader() { public ExtensionLoader extensionLoader() {
return this.loader; return this.loader;
} }
@Override /**
* Gets the extension's logger
*
* @return the extension's logger
*/
public ExtensionLogger logger() { public ExtensionLogger logger() {
return this.logger; return this.logger;
} }
@Override /**
* Gets the {@link GeyserApiBase} instance
*
* @return the {@link GeyserApiBase} instance
*/
public GeyserApiBase geyserApi() { public GeyserApiBase geyserApi() {
return this.api; return this.api;
} }

View file

@ -25,6 +25,9 @@
package org.geysermc.geyser.api.extension.exception; package org.geysermc.geyser.api.extension.exception;
/**
* Thrown when an extension's description is invalid.
*/
public class InvalidDescriptionException extends Exception { public class InvalidDescriptionException extends Exception {
public InvalidDescriptionException(Throwable cause) { public InvalidDescriptionException(Throwable cause) {
super(cause); super(cause);

View file

@ -25,6 +25,9 @@
package org.geysermc.geyser.api.extension.exception; package org.geysermc.geyser.api.extension.exception;
/**
* Thrown when an extension is invalid.
*/
public class InvalidExtensionException extends Exception { public class InvalidExtensionException extends Exception {
public InvalidExtensionException(Throwable cause) { public InvalidExtensionException(Throwable cause) {
super(cause); super(cause);

View file

@ -33,7 +33,7 @@ import java.util.*;
public class GeyserExtensionDescription implements org.geysermc.geyser.api.extension.ExtensionDescription { public class GeyserExtensionDescription implements org.geysermc.geyser.api.extension.ExtensionDescription {
private String name; private String name;
private String main; private String main;
private List<String> api; private String api;
private String version; private String version;
private final List<String> authors = new ArrayList<>(); private final List<String> authors = new ArrayList<>();
@ -47,19 +47,18 @@ public class GeyserExtensionDescription implements org.geysermc.geyser.api.exten
private void loadMap(Map<String, Object> yamlMap) throws InvalidDescriptionException { private void loadMap(Map<String, Object> yamlMap) throws InvalidDescriptionException {
this.name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", ""); this.name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", "");
if (this.name.equals("")) { if (this.name.equals("")) {
throw new InvalidDescriptionException("Invalid extension name"); throw new InvalidDescriptionException("Invalid extension name, cannot be empty");
} }
this.name = this.name.replace(" ", "_"); this.name = this.name.replace(" ", "_");
this.version = String.valueOf(yamlMap.get("version")); this.version = String.valueOf(yamlMap.get("version"));
this.main = (String) yamlMap.get("main"); this.main = (String) yamlMap.get("main");
Object api = yamlMap.get("api"); Object api = yamlMap.get("api");
if (api instanceof List) { if (api instanceof String) {
this.api = (List<String>) api; this.api = (String) api;
} else { } else {
List<String> list = new ArrayList<>(); this.api = "0.0.0";
list.add((String) api); throw new InvalidDescriptionException("Invalid api version format, should be a string: major.minor.patch");
this.api = list;
} }
if (yamlMap.containsKey("author")) { if (yamlMap.containsKey("author")) {
@ -67,7 +66,11 @@ public class GeyserExtensionDescription implements org.geysermc.geyser.api.exten
} }
if (yamlMap.containsKey("authors")) { if (yamlMap.containsKey("authors")) {
this.authors.addAll((Collection<? extends String>) yamlMap.get("authors")); try {
this.authors.addAll((Collection<? extends String>) yamlMap.get("authors"));
} catch (Exception e) {
throw new InvalidDescriptionException("Invalid authors format, should be a list of strings", e);
}
} }
} }
@ -82,7 +85,7 @@ public class GeyserExtensionDescription implements org.geysermc.geyser.api.exten
} }
@Override @Override
public List<String> ApiVersions() { public String ApiVersion() {
return api; return api;
} }

View file

@ -27,7 +27,6 @@ package org.geysermc.geyser.extension;
import org.geysermc.api.Geyser; import org.geysermc.api.Geyser;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.extension.ExtensionLoader; import org.geysermc.geyser.api.extension.ExtensionLoader;
import org.geysermc.geyser.api.extension.GeyserExtension; import org.geysermc.geyser.api.extension.GeyserExtension;
import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
@ -80,7 +79,7 @@ public class GeyserExtensionLoader implements ExtensionLoader {
private void setup(GeyserExtension extension, GeyserExtensionDescription description, File dataFolder, File file) { private void setup(GeyserExtension extension, GeyserExtensionDescription description, File dataFolder, File file) {
GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name()); GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name());
extension.init(Geyser.api(), logger, this, description, dataFolder, file); extension.init(Geyser.api(), this, logger, description, dataFolder, file);
extension.onLoad(); extension.onLoad();
} }
@ -131,7 +130,6 @@ public class GeyserExtensionLoader implements ExtensionLoader {
} }
} }
@Override
public Pattern[] extensionFilters() { public Pattern[] extensionFilters() {
return new Pattern[] { Pattern.compile("^.+\\.jar$") }; return new Pattern[] { Pattern.compile("^.+\\.jar$") };
} }
@ -152,33 +150,29 @@ public class GeyserExtensionLoader implements ExtensionLoader {
} }
} }
public void setClass(String name, final Class<?> clazz) { void setClass(String name, final Class<?> clazz) {
if(!classes.containsKey(name)) { if(!classes.containsKey(name)) {
classes.put(name,clazz); classes.put(name,clazz);
} }
} }
protected void removeClass(String name) { void removeClass(String name) {
Class<?> clazz = classes.remove(name); Class<?> clazz = classes.remove(name);
} }
@Override @Override
public void enableExtension(Extension extension) { public void enableExtension(GeyserExtension extension) {
if (extension instanceof GeyserExtension) { if (!extension.isEnabled()) {
if(!extension.isEnabled()) { GeyserImpl.getInstance().getLogger().info("Enabled extension " + extension.description().name());
GeyserImpl.getInstance().getLogger().info("Enabled extension " + extension.description().name()); extension.setEnabled(true);
((GeyserExtension) extension).setEnabled(true);
}
} }
} }
@Override @Override
public void disableExtension(Extension extension) { public void disableExtension(GeyserExtension extension) {
if (extension instanceof GeyserExtension) { if (extension.isEnabled()) {
if(extension.isEnabled()) { GeyserImpl.getInstance().getLogger().info("Disabled extension " + extension.description().name());
GeyserImpl.getInstance().getLogger().info("Disabled extension " + extension.description().name()); extension.setEnabled(false);
((GeyserExtension) extension).setEnabled(false);
}
} }
} }
} }

View file

@ -26,9 +26,9 @@
package org.geysermc.geyser.extension; package org.geysermc.geyser.extension;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.extension.ExtensionDescription; import org.geysermc.geyser.api.extension.ExtensionDescription;
import org.geysermc.geyser.api.extension.GeyserExtension; import org.geysermc.geyser.api.extension.GeyserExtension;
import java.io.File; import java.io.File;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;
@ -37,35 +37,34 @@ import java.util.regex.Pattern;
public class GeyserExtensionManager { public class GeyserExtensionManager {
private static GeyserExtensionManager geyserExtensionManager = null; private static GeyserExtensionManager geyserExtensionManager = null;
protected Map<String, Extension> extensions = new LinkedHashMap<>(); protected Map<String, GeyserExtension> extensions = new LinkedHashMap<>();
protected Map<Pattern, GeyserExtensionLoader> fileAssociations = new HashMap<>(); protected Map<Pattern, GeyserExtensionLoader> fileAssociations = new HashMap<>();
public static void init() { public static void init() {
GeyserImpl.getInstance().getLogger().info("Loading extensions..."); GeyserImpl.getInstance().getLogger().info("Loading extensions...");
geyserExtensionManager = new GeyserExtensionManager(); geyserExtensionManager = new GeyserExtensionManager();
geyserExtensionManager.registerInterface(GeyserExtensionLoader.class); geyserExtensionManager.registerInterface(GeyserExtensionLoader.class);
geyserExtensionManager.loadExtensions(new File("extensions")); geyserExtensionManager.loadExtensions(new File("extensions"));
GeyserImpl.getInstance().getLogger().info("Loaded " + geyserExtensionManager.extensions.size() + " extensions.");
for (Extension extension : geyserExtensionManager.getExtensions().values()) { String plural = geyserExtensionManager.extensions.size() == 1 ? "" : "s";
if (!extension.isEnabled()) { GeyserImpl.getInstance().getLogger().info("Loaded " + geyserExtensionManager.extensions.size() + " extension" + plural);
geyserExtensionManager.enableExtension(extension);
} geyserExtensionManager.enableExtensions();
}
} }
public static GeyserExtensionManager getExtensionManager() { public static GeyserExtensionManager getExtensionManager() {
return geyserExtensionManager; return geyserExtensionManager;
} }
public Extension getExtension(String name) { public GeyserExtension getExtension(String name) {
if (this.extensions.containsKey(name)) { if (this.extensions.containsKey(name)) {
return this.extensions.get(name); return this.extensions.get(name);
} }
return null; return null;
} }
public Map<String, Extension> getExtensions() { public Map<String, GeyserExtension> getExtensions() {
return this.extensions; return this.extensions;
} }
@ -124,8 +123,8 @@ public class GeyserExtensionManager {
return null; return null;
} }
public Map<String, Extension> loadExtensions(File dictionary) { public Map<String, GeyserExtension> loadExtensions(File dictionary) {
if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) { if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) { // If your IDE says this is always true, ignore it, it isn't.
GeyserImpl.getInstance().getLogger().error("Cannot load extensions in a development environment, aborting extension loading"); GeyserImpl.getInstance().getLogger().error("Cannot load extensions in a development environment, aborting extension loading");
return new HashMap<>(); return new HashMap<>();
} }
@ -134,6 +133,8 @@ public class GeyserExtensionManager {
return new HashMap<>(); return new HashMap<>();
} }
String[] apiVersion = GeyserImpl.VERSION.split("\\.");
if (!dictionary.exists()) { if (!dictionary.exists()) {
dictionary.mkdir(); dictionary.mkdir();
} }
@ -142,7 +143,7 @@ public class GeyserExtensionManager {
} }
Map<String, File> extensions = new LinkedHashMap<>(); Map<String, File> extensions = new LinkedHashMap<>();
Map<String, Extension> loadedExtensions = new LinkedHashMap<>(); Map<String, GeyserExtension> loadedExtensions = new LinkedHashMap<>();
for (final GeyserExtensionLoader loader : this.fileAssociations.values()) { for (final GeyserExtensionLoader loader : this.fileAssociations.values()) {
for (File file : dictionary.listFiles((dir, name) -> { for (File file : dictionary.listFiles((dir, name) -> {
@ -167,47 +168,35 @@ public class GeyserExtensionManager {
continue; continue;
} }
boolean compatible = false; try {
//Check the format: majorVersion.minorVersion.patch
for (String version : description.ApiVersions()) { if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", description.ApiVersion())) {
try { throw new IllegalArgumentException();
//Check the format: majorVersion.minorVersion.patch
if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", version)) {
throw new IllegalArgumentException();
}
} catch (NullPointerException | IllegalArgumentException e) {
GeyserImpl.getInstance().getLogger().error("Could't load extension " + name + ": Wrong API format");
continue;
} }
} catch (NullPointerException | IllegalArgumentException e) {
String[] versionArray = version.split("\\."); GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name + ": Wrong API version format, should be 'majorVersion.minorVersion.patch', current version: " + apiVersion[0] + "." + apiVersion[1]);
String[] apiVersion = GeyserImpl.VERSION.split("\\."); continue;
//Completely different API version
if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name + ": Wrong API version, current version: " + apiVersion[0] + "." + apiVersion[1]);
continue;
}
//If the extension requires new API features, being backwards compatible
if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name + ": Wrong API version, current version: " + apiVersion[0] + "." + apiVersion[1]);
continue;
}
compatible = true;
break;
} }
if (!compatible) { String[] versionArray = description.ApiVersion().split("\\.");
GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name +": Incompatible API version");
//Completely different API version
if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name + ": Wrong API version, current version: " + apiVersion[0] + "." + apiVersion[1]);
continue;
}
//If the extension requires new API features, being backwards compatible
if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name + ": Wrong API version, current version: " + apiVersion[0] + "." + apiVersion[1]);
continue;
} }
extensions.put(name, file); extensions.put(name, file);
loadedExtensions.put(name, this.loadExtension(file, this.fileAssociations)); loadedExtensions.put(name, this.loadExtension(file, this.fileAssociations));
} }
} catch (Exception e) { } catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Couldn't load " +file.getName()+ " in folder " + dictionary + ": ", e); GeyserImpl.getInstance().getLogger().error("Couldn't load " + file.getName() + " in folder " + dictionary.getAbsolutePath() + ": ", e);
} }
} }
} }
@ -215,7 +204,7 @@ public class GeyserExtensionManager {
return loadedExtensions; return loadedExtensions;
} }
public void enableExtension(Extension extension) { public void enableExtension(GeyserExtension extension) {
if (!extension.isEnabled()) { if (!extension.isEnabled()) {
try { try {
extension.extensionLoader().enableExtension(extension); extension.extensionLoader().enableExtension(extension);
@ -226,7 +215,7 @@ public class GeyserExtensionManager {
} }
} }
public void disableExtension(Extension extension) { public void disableExtension(GeyserExtension extension) {
if (extension.isEnabled()) { if (extension.isEnabled()) {
try { try {
extension.extensionLoader().disableExtension(extension); extension.extensionLoader().disableExtension(extension);
@ -236,8 +225,14 @@ public class GeyserExtensionManager {
} }
} }
public void enableExtensions() {
for (GeyserExtension extension : this.getExtensions().values()) {
this.enableExtension(extension);
}
}
public void disableExtensions() { public void disableExtensions() {
for (Extension extension : this.getExtensions().values()) { for (GeyserExtension extension : this.getExtensions().values()) {
this.disableExtension(extension); this.disableExtension(extension);
} }
} }