From 46d859ef30b844b343f722f6a34f1e738e7fa8e1 Mon Sep 17 00:00:00 2001 From: gewuyou <1063891901@qq.com> Date: Wed, 13 Sep 2023 11:46:40 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A9=E6=96=B0=E5=BB=BA=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D=E8=A1=8C=E5=8F=B7?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 ++ src/main/java/module-info.java | 2 + .../event/handler/menubar/OpenFile.java | 2 +- .../jnotepad/model/entity/PluginInfo.java | 23 ++- .../jcnc/jnotepad/plugin/ButtonPlugin.java | 58 ------- .../org/jcnc/jnotepad/plugin/PluginDemo.java | 16 +- .../jcnc/jnotepad/plugin/PluginLoader.java | 98 ++++++++++++ .../jcnc/jnotepad/plugin/PluginManager.java | 60 ++----- .../ui/module/LineNumberTextArea.java | 148 +----------------- .../main/bottom/status/BottomStatusBox.java | 4 +- .../center/main/center/tab/CenterTab.java | 2 +- 11 files changed, 164 insertions(+), 260 deletions(-) delete mode 100644 src/main/java/org/jcnc/jnotepad/plugin/ButtonPlugin.java create mode 100644 src/main/java/org/jcnc/jnotepad/plugin/PluginLoader.java diff --git a/pom.xml b/pom.xml index c52e3e7..9b6996a 100644 --- a/pom.xml +++ b/pom.xml @@ -16,16 +16,26 @@ 20.0.2 + + + + org.fxmisc.richtext + richtextfx + 0.11.1 + + org.kordamp.ikonli ikonli-javafx 12.3.1 + org.kordamp.ikonli ikonli-antdesignicons-pack 12.3.1 + io.github.mkpaz atlantafx-base @@ -66,6 +76,7 @@ logback-classic 1.4.11 + com.ibm.icu icu4j diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index d74a998..392cd89 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -15,6 +15,7 @@ module org.jcnc.jnotepad { requires org.kordamp.ikonli.core; requires org.kordamp.ikonli.javafx; requires org.kordamp.ikonli.antdesignicons; + requires org.fxmisc.richtext; exports org.jcnc.jnotepad; exports org.jcnc.jnotepad.model.enums; exports org.jcnc.jnotepad.app.config; @@ -29,6 +30,7 @@ module org.jcnc.jnotepad { exports org.jcnc.jnotepad.common.interfaces; opens org.jcnc.jnotepad.app.config; exports org.jcnc.jnotepad.views.root.center.main.bottom.status; + exports org.jcnc.jnotepad.plugin.interfaces; exports org.jcnc.jnotepad.ui.dialog; exports org.jcnc.jnotepad.ui.dialog.interfaces; exports org.jcnc.jnotepad.model.entity; diff --git a/src/main/java/org/jcnc/jnotepad/controller/event/handler/menubar/OpenFile.java b/src/main/java/org/jcnc/jnotepad/controller/event/handler/menubar/OpenFile.java index 09a7f5f..a9c1fe1 100644 --- a/src/main/java/org/jcnc/jnotepad/controller/event/handler/menubar/OpenFile.java +++ b/src/main/java/org/jcnc/jnotepad/controller/event/handler/menubar/OpenFile.java @@ -121,7 +121,7 @@ public class OpenFile implements EventHandler { String text = textBuilder.toString(); LogUtil.getLogger(this.getClass()).info("已调用读取文件功能"); Platform.runLater(() -> { - textArea.getMainTextArea().setText(text); + textArea.appendText(text); CenterTab tab = createNewTab(file.getName(), textArea, encoding); // 设置当前标签页关联本地文件 tab.setRelevance(true); diff --git a/src/main/java/org/jcnc/jnotepad/model/entity/PluginInfo.java b/src/main/java/org/jcnc/jnotepad/model/entity/PluginInfo.java index 4616f56..42f1b6b 100644 --- a/src/main/java/org/jcnc/jnotepad/model/entity/PluginInfo.java +++ b/src/main/java/org/jcnc/jnotepad/model/entity/PluginInfo.java @@ -9,7 +9,7 @@ public class PluginInfo { /** * 插件名称 */ - private String pluginName; + private String name; /** * 插件版本 */ @@ -19,12 +19,17 @@ public class PluginInfo { */ private boolean enabled; - public String getPluginName() { - return pluginName; + /** + * 主类名称 + */ + private String mainClass; + + public String getName() { + return name; } - public void setPluginName(String pluginName) { - this.pluginName = pluginName; + public void setName(String name) { + this.name = name; } public String getVersion() { @@ -42,4 +47,12 @@ public class PluginInfo { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public String getMainClass() { + return mainClass; + } + + public void setMainClass(String mainClass) { + this.mainClass = mainClass; + } } diff --git a/src/main/java/org/jcnc/jnotepad/plugin/ButtonPlugin.java b/src/main/java/org/jcnc/jnotepad/plugin/ButtonPlugin.java deleted file mode 100644 index 81d6ac3..0000000 --- a/src/main/java/org/jcnc/jnotepad/plugin/ButtonPlugin.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.jcnc.jnotepad.plugin; - -import org.jcnc.jnotepad.plugin.interfaces.Plugin; -import org.jcnc.jnotepad.util.LogUtil; - -import java.util.Map; - -/** - * 新按钮插件 - * - * @author luke - */ -public class ButtonPlugin implements Plugin { - - @Override - public String getCategoryName() { - return "新按钮插件"; - } - - @Override - public String getDisplayName() { - return "新按钮"; - } - - /** - * 初始化插件 - */ - @Override - public void initialize() { - LogUtil.getLogger(this.getClass()).info("新按钮插件初始化了!"); - } - - @Override - public void execute() { - // 在这里实现新按钮插件的逻辑 - LogUtil.getLogger(this.getClass()).info("新按钮插件执行了!"); - } - - /** - * 获取插件的配置参数 - * - * @return 插件的配置参数 - */ - @Override - public Map getConfig() { - return null; - } - - /** - * 设置插件的配置参数 - * - * @param config 插件的配置参数 - */ - @Override - public void setConfig(Map config) { - - } -} \ No newline at end of file diff --git a/src/main/java/org/jcnc/jnotepad/plugin/PluginDemo.java b/src/main/java/org/jcnc/jnotepad/plugin/PluginDemo.java index af42ec7..8ce4d98 100644 --- a/src/main/java/org/jcnc/jnotepad/plugin/PluginDemo.java +++ b/src/main/java/org/jcnc/jnotepad/plugin/PluginDemo.java @@ -8,7 +8,9 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; import org.jcnc.jnotepad.ui.dialog.factory.impl.BasicFileChooserFactory; import org.jcnc.jnotepad.util.LogUtil; +import org.jcnc.jnotepad.util.PopUpUtil; import org.jcnc.jnotepad.util.UiUtil; +import org.slf4j.Logger; import java.io.File; import java.util.List; @@ -22,6 +24,7 @@ import java.util.Map; * @author luke */ public class PluginDemo { + Logger logger = LogUtil.getLogger(this.getClass()); /** * 启动插件演示界面 @@ -53,8 +56,8 @@ public class PluginDemo { /** * 创建加载插件的按钮 * - * @param primaryStage JavaFX的主舞台 - * @param fileChooser 文件选择器 + * @param primaryStage JavaFX的主舞台 + * @param fileChooser 文件选择器 * @param pluginManager 插件管理器 * @return 加载插件的按钮 */ @@ -65,13 +68,16 @@ public class PluginDemo { File selectedFile = fileChooser.showOpenDialog(primaryStage); if (selectedFile != null) { String pluginFilePath = selectedFile.getAbsolutePath(); - pluginManager.loadPlugins(pluginFilePath); + PluginLoader.getInstance().loadPlugins(pluginFilePath); // 更新插件信息显示 displayPluginInfo(primaryStage, pluginManager); + } else { + PopUpUtil.infoAlert(null, null, "未找到插件!", null, null); + logger.info("未找到插件!"); } - } catch (Exception ignored) { - LogUtil.getLogger(this.getClass()).info("未加载插件!"); + } catch (Exception e) { + logger.error("加载插件失败!", e); } }); return loadButton; diff --git a/src/main/java/org/jcnc/jnotepad/plugin/PluginLoader.java b/src/main/java/org/jcnc/jnotepad/plugin/PluginLoader.java new file mode 100644 index 0000000..9bf5f37 --- /dev/null +++ b/src/main/java/org/jcnc/jnotepad/plugin/PluginLoader.java @@ -0,0 +1,98 @@ +package org.jcnc.jnotepad.plugin; + +import org.jcnc.jnotepad.exception.AppException; +import org.jcnc.jnotepad.model.entity.PluginInfo; +import org.jcnc.jnotepad.plugin.interfaces.Plugin; +import org.jcnc.jnotepad.util.JsonUtil; +import org.jcnc.jnotepad.util.LogUtil; +import org.slf4j.Logger; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +/** + * 插件加载类 + * + * @author gewuyou + */ +public class PluginLoader { + private static final PluginLoader INSTANCE = new PluginLoader(); + Logger logger = LogUtil.getLogger(this.getClass()); + + /** + * 从插件jar包中读取 json 文件到 PluginInfo 类 + * + * @param pluginJar jar 包 + */ + private static PluginInfo readPlugin(File pluginJar) throws IOException { + InputStream is; + StringBuilder sb; + try (JarFile jarFile = new JarFile(pluginJar)) { + ZipEntry zipEntry = jarFile.getEntry("META-INF/plugin.json"); + is = jarFile.getInputStream(zipEntry); + + try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + String temp; + sb = new StringBuilder(); + while ((temp = br.readLine()) != null) { + sb.append(temp); + } + } + } + return JsonUtil.OBJECT_MAPPER.readValue(sb.toString(), PluginInfo.class); + } + + public static PluginLoader getInstance() { + return INSTANCE; + } + + /** + * 加载插件 + * + * @param pluginFilePath 插件文件的路径 + */ + public void loadPlugins(String pluginFilePath) { + PluginManager pluginManager = PluginManager.getInstance(); + List plugins = pluginManager.getPlugins(); + Map> categories = pluginManager.getLoadedPluginsByCategory(); + Map pluginInfos = pluginManager.getPluginInfos(); + File file = new File(pluginFilePath); + if (file.exists() && file.isFile()) { + try { + PluginInfo pluginInfo = readPlugin(file); + pluginInfos.put(pluginInfo.getName(), pluginInfo); + // 创建URLClassLoader以加载Jar文件中的类 + Class pluginClass; + try (URLClassLoader classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()})) { + pluginClass = classLoader.loadClass(pluginInfo.getMainClass()); + } + if (pluginClass == null) { + return; + } + Plugin plugin; + plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); + plugins.add(plugin); + // 添加插件到类别中 + String categoryName = plugin.getCategoryName(); + String displayName = plugin.getDisplayName(); + categories.computeIfAbsent(categoryName, k -> new ArrayList<>()).add(displayName); + } catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new AppException(e); + } catch (ClassNotFoundException e) { + logger.error("无法找到对应的插件类!", e); + } catch (NoSuchMethodException e) { + logger.error("插件类中没有找到指定方法!", e); + } + + } else { + LogUtil.getLogger(this.getClass()).info("PluginInfo file not found: {}", pluginFilePath); + } + } +} diff --git a/src/main/java/org/jcnc/jnotepad/plugin/PluginManager.java b/src/main/java/org/jcnc/jnotepad/plugin/PluginManager.java index 8c236fa..380276a 100644 --- a/src/main/java/org/jcnc/jnotepad/plugin/PluginManager.java +++ b/src/main/java/org/jcnc/jnotepad/plugin/PluginManager.java @@ -1,14 +1,10 @@ package org.jcnc.jnotepad.plugin; +import org.jcnc.jnotepad.model.entity.PluginInfo; import org.jcnc.jnotepad.plugin.interfaces.Plugin; import org.jcnc.jnotepad.util.LogUtil; import org.slf4j.Logger; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -33,6 +29,11 @@ public class PluginManager { * 插件类别 */ private final Map> categories = new HashMap<>(); + /** + * 插件信息 + */ + + private final Map pluginInfos = new HashMap<>(); private PluginManager() { @@ -42,47 +43,6 @@ public class PluginManager { return INSTANCE; } - /** - * 加载插件 - * - * @param pluginFilePath 插件文件的路径 - */ - public void loadPlugins(String pluginFilePath) { - File file = new File(pluginFilePath); - if (file.exists() && file.isFile()) { - // 创建URLClassLoader以加载Jar文件中的类 - Class pluginClass = null; - try (URLClassLoader classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()})) { - pluginClass = classLoader.loadClass("org.jcnc.jnotepad.plugin.ButtonPlugin"); - } catch (ClassNotFoundException e) { - logger.error("无法找到对应的插件类!", e); - } catch (MalformedURLException e) { - logger.error("无法创建URL格式错误!", e); - } catch (IOException e) { - logger.error("IO异常!", e); - } - if (pluginClass == null) { - return; - } - Plugin plugin = null; - try { - plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); - } catch (Exception e) { - logger.error("发生异常!", e); - } - if (plugin == null) { - return; - } - plugins.add(plugin); - - // 添加插件到类别中 - String categoryName = plugin.getCategoryName(); - String displayName = plugin.getDisplayName(); - categories.computeIfAbsent(categoryName, k -> new ArrayList<>()).add(displayName); - } else { - LogUtil.getLogger(this.getClass()).info("PluginInfo file not found: {}", pluginFilePath); - } - } /** * 卸载插件 @@ -122,4 +82,12 @@ public class PluginManager { public Map> getLoadedPluginsByCategory() { return categories; } + + public List getPlugins() { + return plugins; + } + + public Map getPluginInfos() { + return pluginInfos; + } } diff --git a/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java b/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java index 909b483..27c969e 100644 --- a/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java +++ b/src/main/java/org/jcnc/jnotepad/ui/module/LineNumberTextArea.java @@ -1,9 +1,7 @@ package org.jcnc.jnotepad.ui.module; -import javafx.beans.property.StringProperty; -import javafx.scene.control.TextArea; -import javafx.scene.layout.BorderPane; -import org.jcnc.jnotepad.controller.config.AppConfigController; +import org.fxmisc.richtext.CodeArea; +import org.fxmisc.richtext.LineNumberFactory; import org.jcnc.jnotepad.util.LogUtil; import org.jcnc.jnotepad.views.root.center.main.bottom.status.BottomStatusBox; import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTab; @@ -22,84 +20,36 @@ import java.io.IOException; * * @author luke */ -public class LineNumberTextArea extends BorderPane { +public class LineNumberTextArea extends CodeArea { - /** - * 用于确定行号区域宽度的大小表格,每个元素表示不同的行数范围 - */ - private static final int[] SIZE_TABLE = {9, 99, 999, 9999, 99999, 999999, 9999999, - 99999999, 999999999, Integer.MAX_VALUE}; /** * 用于记录日志的静态Logger对象 */ private static final Logger logger = LogUtil.getLogger(LineNumberTextArea.class); - /** - * 行号区域的最小宽度 - */ - private static final int MIN_LINE_NUMBER_WIDTH = 30; - - /** - * 主文本区域的TextArea实例 - */ - private final TextArea mainTextArea = new TextArea(); - - /** - * 行号区域的TextArea实例 - */ - private final TextArea lineNumberArea = new TextArea(); - - /** * 构造函数 *

* 用于创建 LineNumberTextArea 对象 */ public LineNumberTextArea() { - // 设置主文本区域是否自动换行,根据应用配置决定 - mainTextArea.setWrapText(AppConfigController.getInstance().getAutoLineConfig()); - - // 设置行号区域不可编辑 - lineNumberArea.setEditable(false); - - // 设置行号区域的首选宽度和最小宽度为最小行号宽度 - lineNumberArea.setPrefWidth(MIN_LINE_NUMBER_WIDTH); - lineNumberArea.setMinWidth(MIN_LINE_NUMBER_WIDTH); - - // 为行号区域和主文本区域添加CSS样式类 - lineNumberArea.getStyleClass().add("text-line-number"); - mainTextArea.getStyleClass().add("main-text-area"); - // 设置 LineNumberTextArea 的样式,包括边框和背景颜色 this.setStyle( "-fx-border-color:white;" + "-fx-background-color:white" ); - - // 初始化监听器,用于处理事件 + this.setParagraphGraphicFactory(LineNumberFactory.get(this)); initListeners(); - // 将主文本区域设置为中央内容,将行号区域设置为左侧内容 - setCenter(mainTextArea); - setLeft(lineNumberArea); - } + } /** * 初始化监听器方法 */ private void initListeners() { - // 监听主要文本区域的滚动位置变化 - mainTextArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> lineNumberArea.setScrollTop(mainTextArea.getScrollTop())); - // 监听行号文本区域的滚动位置变化 - lineNumberArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> mainTextArea.setScrollTop(lineNumberArea.getScrollTop())); - // 监听行号文本区域的文本变化 - lineNumberArea.textProperty().addListener((observable, oldValue, newValue) -> updateLineNumberWidth()); - // 监听主要文本区域的光标位置变化 - this.mainTextArea.caretPositionProperty().addListener((caretObservable, oldPosition, newPosition) -> BottomStatusBox.getInstance().updateWordCountStatusLabel()); // 监听主要文本区域的文本变化 this.textProperty().addListener((observable, oldValue, newValue) -> { - updateLineNumberArea(); BottomStatusBox.getInstance().updateWordCountStatusLabel(); save(); }); @@ -121,7 +71,7 @@ public class LineNumberTextArea extends BorderPane { File file = (File) tab.getUserData(); // 获取主文本区域中的文本内容 - String newValue = this.mainTextArea.getText(); + String newValue = this.getText(); // 如果文件对象为空,记录警告信息并返回,不执行保存操作 if (file == null) { @@ -140,90 +90,4 @@ public class LineNumberTextArea extends BorderPane { LogUtil.getLogger(this.getClass()).info("已忽视IO异常!"); } } - - - /** - * 更新行号宽度方法 - */ - private void updateLineNumberWidth() { - // 获取主文本区域的段落数量,即文本的行数 - int numOfLines = mainTextArea.getParagraphs().size(); - - // 初始化一个计数器,用于确定适合的行号宽度 - int count = 1; - - // 遍历行号宽度大小的表格 - for (int i = 0; i < SIZE_TABLE.length; i++) { - // 检查文本行数是否在当前表格项的范围内 - if (numOfLines <= SIZE_TABLE[i]) { - // 如果是,设置计数器为当前索引+1并退出循环 - count = i + 1; - break; - } - } - - // 计算实际的行号区域宽度,确保不小于一个最小宽度值 - int actualWidth = Math.max(count * 10 + 11, MIN_LINE_NUMBER_WIDTH); - - // 检查实际宽度是否与当前行号区域的宽度不同 - if (actualWidth != lineNumberArea.getWidth()) { - // 如果不同,设置行号区域的首选宽度为实际宽度 - lineNumberArea.setPrefWidth(actualWidth); - } - } - - - /** - * 获取主要文本区域的text属性 - * - * @return 主要文本区域的text属性 - */ - public StringProperty textProperty() { - return mainTextArea.textProperty(); - } - - /** - * 更新行号区域方法 - */ - private void updateLineNumberArea() { - // 获取主文本区域的垂直滚动位置 - double mainTextAreaScrollTop = mainTextArea.getScrollTop(); - - // 获取行号区域的垂直滚动位置 - double lineNumberAreaScrollTop = lineNumberArea.getScrollTop(); - - // 获取主文本区域的段落数量,即文本的行数 - int numOfLines = mainTextArea.getParagraphs().size(); - - // 用于构建行号文本的字符串构建器 - StringBuilder lineNumberText = new StringBuilder(); - - // 循环迭代,生成行号文本, - for (int i = 1; i <= numOfLines; i++) { - // 将行号和换行符添加到字符串中 - lineNumberText.append(i); - if (i != numOfLines) { - lineNumberText.append("\n"); - } - } - - // 将生成的行号文本设置到 行号区域 - lineNumberArea.setText(lineNumberText.toString()); - - // 恢复主文本区域的垂直滚动位置 - mainTextArea.setScrollTop(mainTextAreaScrollTop); - - // 恢复行号区域的垂直滚动位置 - lineNumberArea.setScrollTop(lineNumberAreaScrollTop); - } - - - /** - * 获取主要文本区域 - * - * @return 主要文本区域 - */ - public TextArea getMainTextArea() { - return mainTextArea; - } } \ No newline at end of file diff --git a/src/main/java/org/jcnc/jnotepad/views/root/center/main/bottom/status/BottomStatusBox.java b/src/main/java/org/jcnc/jnotepad/views/root/center/main/bottom/status/BottomStatusBox.java index eeee545..4a064dc 100644 --- a/src/main/java/org/jcnc/jnotepad/views/root/center/main/bottom/status/BottomStatusBox.java +++ b/src/main/java/org/jcnc/jnotepad/views/root/center/main/bottom/status/BottomStatusBox.java @@ -3,10 +3,10 @@ package org.jcnc.jnotepad.views.root.center.main.bottom.status; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; -import javafx.scene.control.TextArea; import org.jcnc.jnotepad.app.i18n.UiResourceBundle; import org.jcnc.jnotepad.common.constants.TextConstants; import org.jcnc.jnotepad.ui.module.AbstractHorizontalBox; +import org.jcnc.jnotepad.ui.module.LineNumberTextArea; import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTab; import org.jcnc.jnotepad.views.root.center.main.center.tab.CenterTabPane; @@ -94,7 +94,7 @@ public class BottomStatusBox extends AbstractHorizontalBox { if (instance.getSelected() == null) { return; } - TextArea textArea = instance.getSelected().getLineNumberTextArea().getMainTextArea(); + LineNumberTextArea textArea = instance.getSelected().getLineNumberTextArea(); int caretPosition = textArea.getCaretPosition(); int row = getRow(caretPosition, textArea.getText()); int column = getColumn(caretPosition, textArea.getText()); diff --git a/src/main/java/org/jcnc/jnotepad/views/root/center/main/center/tab/CenterTab.java b/src/main/java/org/jcnc/jnotepad/views/root/center/main/center/tab/CenterTab.java index 1cb56db..db93f42 100644 --- a/src/main/java/org/jcnc/jnotepad/views/root/center/main/center/tab/CenterTab.java +++ b/src/main/java/org/jcnc/jnotepad/views/root/center/main/center/tab/CenterTab.java @@ -56,7 +56,7 @@ public class CenterTab extends Tab { public void setAutoLine(boolean autoLine) { this.autoLine = autoLine; - lineNumberTextArea.getMainTextArea().setWrapText(autoLine); + lineNumberTextArea.setWrapText(autoLine); } public LineNumberTextArea getLineNumberTextArea() {