diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..7a7f31f --- /dev/null +++ b/.classpath @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/META-INF/manifest.xml b/META-INF/manifest.xml deleted file mode 100644 index b8e7ac6..0000000 --- a/META-INF/manifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build.gradle b/build.gradle index be59f7b..d30d9ea 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,64 @@ +apply plugin: 'java' +apply plugin: 'eclipse' +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +test { + useJUnitPlatform() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + compileClasspath 'org.libreoffice:jurt:5.3.2' + compileClasspath 'org.libreoffice:juh:5.3.2' + compileClasspath 'org.libreoffice:ridl:5.3.2' + compileClasspath 'org.libreoffice:unoil:5.3.2' + compileClasspath group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' + compileClasspath 'ch.qos.logback:logback-classic:1.2.6' + compileClasspath 'ch.qos.logback:logback-core:1.2.6' + compileClasspath 'org.apache.tika:tika-core:2.1.0' +} + + +task oxtjar(type: Jar) { + archiveName 'docsettings_oxt.jar' + manifest { + attributes("Implementation-Title": rootProject.name, + "Implementation-Version": project.version, + "RegistrationClassName" : "pro.litvinovg.docsettings.RegistrationHandler", + "Class-Path" : "jasp.jar parser.jar") + } + from('src/main/java') { + include '**/*.classes' + include 'logback.xml' + include 'pro/litvinovg/docsettings/resources/*.png' + include 'pro/litvinovg/docsettings/localizations/*.properties' + } + from (sourceSets.main.output){ + include 'pro/litvinovg/**/*.class' + include 'ch/**/*' + include 'org/slf4j/**/*' + } + from { + configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + + task oxt(type: Zip) { - from './' + dependsOn = [ 'oxtjar' ] + from './oxt/' include '*' include '*/*' include '*/*/*' include '*/*/*/*' - exclude 'docSettings.oxt' exclude '.*' - exclude 'build.gradle' - exclude 'translations.ods' - exclude 'update.xml' + from ('build/libs/docsettings_oxt.jar'){ + include '*' + } archiveName 'docSettings.oxt' } diff --git a/description/description_en.txt b/description/description_en.txt deleted file mode 100644 index ac3eafb..0000000 --- a/description/description_en.txt +++ /dev/null @@ -1,3 +0,0 @@ -Extension for document settings configuration. -For now consists of workaround for layout bug since LO 6.4 -https://bugs.documentfoundation.org/show_bug.cgi?id=134782 diff --git a/description/description_ru.txt b/description/description_ru.txt deleted file mode 100644 index b586ade..0000000 --- a/description/description_ru.txt +++ /dev/null @@ -1,3 +0,0 @@ -Расширение исправления настроек документов -В данный момент содержит исправления для бага, возникшего с версии 6.4 -https://bugs.documentfoundation.org/show_bug.cgi?id=134782 diff --git a/docSettings/Events.xba b/docSettings/Events.xba deleted file mode 100644 index 9fd087c..0000000 --- a/docSettings/Events.xba +++ /dev/null @@ -1,14 +0,0 @@ - - -Sub execOnDocumentLoad - Dim fixCompatibilityOption As Boolean - fixCompatibilityOption = true - If fixCompatibilityOption Then - Dim oDocSettings As Object - oDocSettings = ThisComponent.createInstance( "com.sun.star.document.Settings" ) - If oDocSettings.supportsService("com.sun.star.text.DocumentSettings") Then - oDocSettings.setPropertyValue("AddParaSpacingToTableCells", false) - EndIf - EndIf -End Sub - \ No newline at end of file diff --git a/docSettings/dialog.xlb b/docSettings/dialog.xlb deleted file mode 100644 index f679cd9..0000000 --- a/docSettings/dialog.xlb +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docSettings/script.xlb b/docSettings/script.xlb deleted file mode 100644 index 9077bd7..0000000 --- a/docSettings/script.xlb +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/oxt/Addons.xcu b/oxt/Addons.xcu new file mode 100644 index 0000000..5e5150b --- /dev/null +++ b/oxt/Addons.xcu @@ -0,0 +1,46 @@ + + + + + + + + service:pro.litvinovg.libreoffice.DocSettings?openGUI + + + + vnd.sun.star.extension://pro.litvinovg.libreoffice.docsettings/icons/button_icon.png + + + + + + + + + Document settings panel + + true + + + service:pro.litvinovg.libreoffice.DocSettings?openGUI + + + _self + + + com.sun.star.text.TextDocument + + + Open document settings + Открыть настройки документа + + + + + + + + + diff --git a/Events.xcu b/oxt/Events.xcu similarity index 70% rename from Events.xcu rename to oxt/Events.xcu index 95e1d37..d632a08 100644 --- a/Events.xcu +++ b/oxt/Events.xcu @@ -4,9 +4,9 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="Events" oor:package="org.openoffice.Office"> - + - vnd.sun.star.script:docSettings.Events.execOnDocumentLoad?language=Basic&location=application + service:pro.litvinovg.libreoffice.DocSettings?onLoad diff --git a/oxt/META-INF/manifest.xml b/oxt/META-INF/manifest.xml new file mode 100644 index 0000000..5678c7f --- /dev/null +++ b/oxt/META-INF/manifest.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/oxt/WriterWindowState.xcu b/oxt/WriterWindowState.xcu new file mode 100644 index 0000000..65aa27e --- /dev/null +++ b/oxt/WriterWindowState.xcu @@ -0,0 +1,27 @@ + + + + + + + + + Document settings + Настройки документа + + + 10,0 + + + true + + + 0 + + + + + + diff --git a/description.xml b/oxt/description.xml similarity index 83% rename from description.xml rename to oxt/description.xml index 3cd203d..7ffe423 100644 --- a/description.xml +++ b/oxt/description.xml @@ -1,11 +1,11 @@ - + - Исправление свойств документов - Docement settings fix + Управление настройками документов + Docement settings management @@ -21,6 +21,7 @@ Georgy Litvinov + Георгий Литвинов diff --git a/oxt/description/description_en.txt b/oxt/description/description_en.txt new file mode 100644 index 0000000..227b0ef --- /dev/null +++ b/oxt/description/description_en.txt @@ -0,0 +1,2 @@ +Extension for document settings configuration. +Provides a way to create setttings backups and restore from it. diff --git a/oxt/description/description_ru.txt b/oxt/description/description_ru.txt new file mode 100644 index 0000000..7bfc9de --- /dev/null +++ b/oxt/description/description_ru.txt @@ -0,0 +1,2 @@ +Расширение для управления настройками документов +Предоставляет возможность создания резервных копий настроек и восстановления из них. diff --git a/icons/addon_icon.png b/oxt/icons/addon_icon.png similarity index 100% rename from icons/addon_icon.png rename to oxt/icons/addon_icon.png diff --git a/oxt/icons/button_icon.png b/oxt/icons/button_icon.png new file mode 100644 index 0000000..e89adc0 Binary files /dev/null and b/oxt/icons/button_icon.png differ diff --git a/license_en.txt b/oxt/license_en.txt similarity index 97% rename from license_en.txt rename to oxt/license_en.txt index daea221..2fa6114 100644 --- a/license_en.txt +++ b/oxt/license_en.txt @@ -1,6 +1,6 @@ The author of this extension is: Georgy Litvinov (public@litvinovg.pro) -Copyright 2020. +Copyright 2021. This library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 of the License. diff --git a/license_ru.txt b/oxt/license_ru.txt similarity index 98% rename from license_ru.txt rename to oxt/license_ru.txt index 44f691a..b970b21 100644 --- a/license_ru.txt +++ b/oxt/license_ru.txt @@ -1,6 +1,6 @@ Автор расширения: Георгий Литвинов (public@litvinovg.pro) -Copyright 2020. +Copyright 2021. Это расширение является свободным программным обеспечением, вы можете распространять и/или модифицировать его в соответствии с лицензией GNU Lesser General Public License версии 3.0, опубликованной Free Software Foundation. diff --git a/releasenotes.txt b/oxt/releasenotes.txt similarity index 63% rename from releasenotes.txt rename to oxt/releasenotes.txt index 3d2554f..e3b59b3 100644 --- a/releasenotes.txt +++ b/oxt/releasenotes.txt @@ -1,2 +1,4 @@ +0.0.6 Added settings backup and restore options +0.0.5 Removed obsoleted workaround 0.0.3 Apply only to writer documents 0.0.1 First public release with workaround for https://bugs.documentfoundation.org/show_bug.cgi?id=134782 diff --git a/src/main/java/logback.xml b/src/main/java/logback.xml new file mode 100644 index 0000000..ff31ee6 --- /dev/null +++ b/src/main/java/logback.xml @@ -0,0 +1,24 @@ + + + + + %d{HH:mm:ss.SSS} %-5level - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/pro/litvinovg/docsettings/DialogHelper.java b/src/main/java/pro/litvinovg/docsettings/DialogHelper.java new file mode 100644 index 0000000..3b9deac --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/DialogHelper.java @@ -0,0 +1,185 @@ +package pro.litvinovg.docsettings; + + +import java.io.File; + +import com.sun.star.awt.MessageBoxType; +import com.sun.star.awt.Point; +import com.sun.star.awt.XButton; +import com.sun.star.awt.XComboBox; +import com.sun.star.awt.XControl; +import com.sun.star.awt.XControlContainer; +import com.sun.star.awt.XControlModel; +import com.sun.star.awt.XDialog; +import com.sun.star.awt.XDialogEventHandler; +import com.sun.star.awt.XDialogProvider2; +import com.sun.star.awt.XFixedText; +import com.sun.star.awt.XListBox; +import com.sun.star.awt.XMessageBox; +import com.sun.star.awt.XMessageBoxFactory; +import com.sun.star.awt.XTextComponent; +import com.sun.star.awt.XToolkit; +import com.sun.star.awt.XWindow; +import com.sun.star.awt.XWindowPeer; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.beans.UnknownPropertyException; +import com.sun.star.beans.XPropertySet; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.uno.Exception; +import com.sun.star.uno.UnoRuntime; +import com.sun.star.uno.XComponentContext; + +public class DialogHelper { + + /** + * Create a dialog from an xdl file. + * + * @param xdlFile + * The filename in the `dialog` folder + * @param context + * @return XDialog + */ + public static XDialog createDialog(String xdlFile, XComponentContext context, XDialogEventHandler handler) { + Object oDialogProvider; + try { + oDialogProvider = context.getServiceManager().createInstanceWithContext("com.sun.star.awt.DialogProvider2", + context); + XDialogProvider2 xDialogProv = (XDialogProvider2) UnoRuntime.queryInterface(XDialogProvider2.class, + oDialogProvider); + File dialogFile = FileHelper.getDialogFilePath(xdlFile, context); + return xDialogProv.createDialogWithHandler(convertToURL(context, dialogFile), handler); + } catch (Exception e) { + return null; + } + } + + /** Returns a URL to be used with XDialogProvider to create a dialog */ + public static String convertToURL(XComponentContext xContext, File dialogFile) { + String sURL = null; + try { + com.sun.star.ucb.XFileIdentifierConverter xFileConverter = (com.sun.star.ucb.XFileIdentifierConverter) UnoRuntime + .queryInterface(com.sun.star.ucb.XFileIdentifierConverter.class, xContext.getServiceManager() + .createInstanceWithContext("com.sun.star.ucb.FileContentProvider", xContext)); + sURL = xFileConverter.getFileURLFromSystemPath("", dialogFile.getAbsolutePath()); + } catch (com.sun.star.uno.Exception ex) { + return null; + } + return sURL; + } + + /** Returns a button (XButton) from a dialog */ + public static XButton getButton(XDialog dialog, String componentId) { + XControlContainer xDlgContainer = (XControlContainer) UnoRuntime.queryInterface(XControlContainer.class, + dialog); + Object control = xDlgContainer.getControl(componentId); + return (XButton) UnoRuntime.queryInterface(XButton.class, control); + } + + /** Returns a text field (XTextComponent) from a dialog */ + public static XTextComponent getEditField(XDialog dialog, String componentId) { + XControlContainer xDlgContainer = (XControlContainer) UnoRuntime.queryInterface(XControlContainer.class, + dialog); + Object control = xDlgContainer.getControl(componentId); + return (XTextComponent) UnoRuntime.queryInterface(XTextComponent.class, control); + } + + /** Returns a Combo box (XComboBox) from a dialog */ + public static XComboBox getCombobox(XDialog dialog, String componentId) { + XControlContainer xDlgContainer = (XControlContainer) UnoRuntime.queryInterface(XControlContainer.class, + dialog); + Object control = xDlgContainer.getControl(componentId); + return (XComboBox) UnoRuntime.queryInterface(XComboBox.class, control); + } + + /** Returns a List box (XListBox) from a dialog */ + public static XListBox getListBox(XDialog dialog, String componentId) { + XControlContainer xDlgContainer = (XControlContainer) UnoRuntime.queryInterface(XControlContainer.class, + dialog); + Object control = xDlgContainer.getControl(componentId); + return (XListBox) UnoRuntime.queryInterface(XListBox.class, control); + } + + /** Returns a label (XFixedText) from a dialog */ + public static XFixedText getLabel(XDialog dialog, String componentId) { + XControlContainer xDlgContainer = (XControlContainer) UnoRuntime.queryInterface(XControlContainer.class, + dialog); + Object control = xDlgContainer.getControl(componentId); + return (XFixedText) UnoRuntime.queryInterface(XFixedText.class, control); + } + + public static void EnableButton(XDialog dialog, String componentId, boolean enable) { + XControlContainer xDlgContainer = (XControlContainer) UnoRuntime.queryInterface(XControlContainer.class, + dialog); + // retrieve the control that we want to disable or enable + XControl xControl = UnoRuntime.queryInterface(XControl.class, xDlgContainer.getControl(componentId)); + XPropertySet xModelPropertySet = UnoRuntime.queryInterface(XPropertySet.class, xControl.getModel()); + try { + xModelPropertySet.setPropertyValue("Enabled", Boolean.valueOf(enable)); + } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException + | WrappedTargetException e) { + return; + } + } + + /** Set the focus to an input field */ + public static void SetFocus(XTextComponent editField) { + XWindow xControlWindow = UnoRuntime.queryInterface(XWindow.class, editField); + xControlWindow.setFocus(); + } + + public static void setPosition(XDialog dialog, int posX, int posY) { + XControlModel xDialogModel = UnoRuntime.queryInterface(XControl.class, dialog).getModel(); + XPropertySet xPropSet = UnoRuntime.queryInterface(XPropertySet.class, xDialogModel); + try { + xPropSet.setPropertyValue("PositionX", posX); + xPropSet.setPropertyValue("PositionY", posY); + } catch (com.sun.star.lang.IllegalArgumentException | UnknownPropertyException | PropertyVetoException + | WrappedTargetException e) { + return; + } + } + + public static Point getPosition(XDialog dialog) { + int posX = 0; + int posY = 0; + XControlModel xDialogModel = UnoRuntime.queryInterface(XControl.class, dialog).getModel(); + XPropertySet xPropSet = UnoRuntime.queryInterface(XPropertySet.class, xDialogModel); + try { + posX = (int) xPropSet.getPropertyValue("PositionX"); + posY = (int) xPropSet.getPropertyValue("PositionY"); + } catch (UnknownPropertyException | WrappedTargetException e) { + } + return new Point(posX, posY); + } + + public static void showInfoMessage(XComponentContext context, XDialog dialog, String message) { + showMessageBox(context, dialog, MessageBoxType.INFOBOX, "Info", message); + } + + public static void showWarningMessage(XComponentContext context, XDialog dialog, String message) { + showMessageBox(context, dialog, MessageBoxType.WARNINGBOX, "Warning", message); + } + + public static void showErrorMessage(XComponentContext context, XDialog dialog, String message) { + showMessageBox(context, dialog, MessageBoxType.ERRORBOX, "Error", message); + } + + public static void showMessageBox(XComponentContext context, XDialog dialog, MessageBoxType type, String sTitle, String sMessage) { + XToolkit xToolkit; + try { + xToolkit = UnoRuntime.queryInterface(XToolkit.class, + context.getServiceManager().createInstanceWithContext("com.sun.star.awt.Toolkit", context)); + } catch (Exception e) { + return; + } + XMessageBoxFactory xMessageBoxFactory = UnoRuntime.queryInterface(XMessageBoxFactory.class, xToolkit); + XWindowPeer xParentWindowPeer = UnoRuntime.queryInterface(XWindowPeer.class, dialog); + XMessageBox xMessageBox = xMessageBoxFactory.createMessageBox(xParentWindowPeer, type, + com.sun.star.awt.MessageBoxButtons.BUTTONS_OK, sTitle, sMessage); + if (xMessageBox == null) + return; + + xMessageBox.execute(); + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/DocSettingsExtension.java b/src/main/java/pro/litvinovg/docsettings/DocSettingsExtension.java new file mode 100644 index 0000000..d9ea1ea --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/DocSettingsExtension.java @@ -0,0 +1,121 @@ +package pro.litvinovg.docsettings; + +import java.awt.EventQueue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.star.lang.XSingleComponentFactory; + +//import pro.litvinovg.docsettings.gui.ConfigurationWindow; +//import pro.litvinovg.docsettings.gui.ConversionExecutor; + +import com.sun.star.lib.uno.helper.Factory; +import com.sun.star.lib.uno.helper.WeakBase; +import com.sun.star.registry.XRegistryKey; +import com.sun.star.uno.XComponentContext; + +import pro.litvinovg.docsettings.document.ODFDocument; +import pro.litvinovg.docsettings.gui.MainWindow; +import pro.litvinovg.docsettings.settings.Backups; +import pro.litvinovg.docsettings.settings.Settings; + +public final class DocSettingsExtension extends WeakBase + implements com.sun.star.lang.XServiceInfo, com.sun.star.task.XJobExecutor { + private final XComponentContext context; + private static final String m_implementationName = DocSettingsExtension.class.getName(); + private static final String[] m_serviceNames = { "pro.litvinovg.libreoffice.DocSettings" }; + private static final Logger logger = LoggerFactory.getLogger(DocSettingsExtension.class); + + public DocSettingsExtension(XComponentContext componentContext) { + context = componentContext; + }; + + public static XSingleComponentFactory __getComponentFactory(String sImplementationName) { + XSingleComponentFactory xFactory = null; + + if (sImplementationName.equals(m_implementationName)) + xFactory = Factory.createComponentFactory(DocSettingsExtension.class, m_serviceNames); + return xFactory; + } + + public static boolean __writeRegistryServiceInfo(XRegistryKey xRegistryKey) { + return Factory.writeRegistryServiceInfo(m_implementationName, m_serviceNames, xRegistryKey); + } + + // com.sun.star.lang.XServiceInfo: + public String getImplementationName() { + return m_implementationName; + } + + public boolean supportsService(String sService) { + int len = m_serviceNames.length; + + for (int i = 0; i < len; i++) { + if (sService.equals(m_serviceNames[i])) + return true; + } + return false; + } + + public String[] getSupportedServiceNames() { + return m_serviceNames; + } + + // com.sun.star.task.XJobExecutor: + public void trigger(String action) { + switch (action) { + case "openGUI": + try { + MainWindow.runGUI(context); + } catch (Throwable e) { + DialogHelper.showErrorMessage(context, null, e.getLocalizedMessage()); + } + break; + case "onLoad": + try { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + onLoad(); + } catch (Exception e) { + DialogHelper.showErrorMessage(context, null, e.getLocalizedMessage()); + } + } + }); + } catch (Throwable e) { + DialogHelper.showErrorMessage(context, null, e.getLocalizedMessage()); + } + break; + default: + DialogHelper.showErrorMessage(context, null, "Unknown action: " + action); + } + } + + private void onLoad() { + ODFDocument document = null; + try { + document = new ODFDocument(context); + } catch (Exception e) { + logger.debug("Ops", e); + return; + } + Backups backups = new Backups(); + ExtensionConfig config = ExtensionConfig.getInstance(); + document.readSettings(); + document.readBackups(backups); + if (!document.hasBackups(backups)) { + backups.create(document.getCurrentSettings()); + document.writeBackups(backups); + return; + } + if (config.getAutoRestore()) { + Settings settings = backups.getLatest(); + boolean isSettingsChanged = document.compareWithBackup(settings); + if (isSettingsChanged) { + document.restoreSettings(settings); + } + } + + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/ExtensionConfig.java b/src/main/java/pro/litvinovg/docsettings/ExtensionConfig.java new file mode 100644 index 0000000..050d96b --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/ExtensionConfig.java @@ -0,0 +1,41 @@ +package pro.litvinovg.docsettings; + +import java.util.HashMap; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ExtensionConfig { + + private static final Logger logger = LoggerFactory.getLogger(ExtensionConfig.class); + private static final String AUTO_RESTORE = "autoRestore"; + private static ExtensionConfig extensionSettings = null; + private Preferences storage; + + public static ExtensionConfig getInstance() { + if (extensionSettings != null) { + return extensionSettings; + } + extensionSettings = new ExtensionConfig(); + return extensionSettings; + } + + private ExtensionConfig() { + storage = Preferences.userRoot().node(this.getClass().getName()); + } + + public boolean getAutoRestore() { + return storage.getBoolean(AUTO_RESTORE, false); + } + + public void setAutoRestore(boolean value) { + storage.putBoolean(AUTO_RESTORE, value); + try { + storage.sync(); + } catch (BackingStoreException e) { + logger.debug("Ops", e); + } + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/FileHelper.java b/src/main/java/pro/litvinovg/docsettings/FileHelper.java new file mode 100644 index 0000000..458c5d1 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/FileHelper.java @@ -0,0 +1,60 @@ +package pro.litvinovg.docsettings; + + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; + +import com.sun.star.deployment.PackageInformationProvider; +import com.sun.star.deployment.XPackageInformationProvider; +import com.sun.star.uno.Exception; +import com.sun.star.uno.UnoRuntime; +import com.sun.star.uno.XComponentContext; +import com.sun.star.util.XURLTransformer; + +public class FileHelper { + + final static String DIALOG_RESOURCES = "dialog/"; + + /** + * Returns a path to a dialog file + */ + public static File getDialogFilePath(String xdlFile, XComponentContext xContext) { + return getFilePath(DIALOG_RESOURCES + xdlFile, xContext); + } + + /** + * Returns a file path for a file in the installed extension, or null on failure. + */ + public static File getFilePath(String file, XComponentContext xContext) { + XPackageInformationProvider xPackageInformationProvider = PackageInformationProvider.get(xContext); + String location = xPackageInformationProvider.getPackageLocation("org.libreoffice.example.starterproject"); + Object oTransformer; + try { + oTransformer = xContext.getServiceManager().createInstanceWithContext("com.sun.star.util.URLTransformer", xContext); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + XURLTransformer xTransformer = (XURLTransformer)UnoRuntime.queryInterface(XURLTransformer.class, oTransformer); + com.sun.star.util.URL[] oURL = new com.sun.star.util.URL[1]; + oURL[0] = new com.sun.star.util.URL(); + oURL[0].Complete = location + "/" + file; + xTransformer.parseStrict(oURL); + URL url; + try { + url = new URL(oURL[0].Complete); + } catch (MalformedURLException e1) { + return null; + } + File f; + try { + f = new File(url.toURI()); + } catch (URISyntaxException e1) { + return null; + } + return f; + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/Localizer.java b/src/main/java/pro/litvinovg/docsettings/Localizer.java new file mode 100644 index 0000000..61bf112 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/Localizer.java @@ -0,0 +1,62 @@ +package pro.litvinovg.docsettings; + +import java.io.UnsupportedEncodingException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; + +public class Localizer { + + private Locale currentLocale; + private String resourceBundle = "pro.litvinovg.docsettings.localizations.docsettings"; + private ResourceBundle translations; + private boolean isOld = false; + private static Localizer localizer = null; + private Localizer() { + currentLocale = Locale.getDefault(); + translations = ResourceBundle.getBundle(resourceBundle, currentLocale); + this.isOld = isOldJava(); + } + + public static Localizer getInstance() { + if (localizer != null) { + return localizer; + } + localizer = new Localizer(); + return localizer; + } + public static String get(String name) { + Localizer.getInstance(); + return localizer.getTranslation(name); + } + + public String getTranslation(String name) { + if (translations != null && translations.containsKey(name)) { + String translation = translations.getString(name); + if (isOld) { + try { + return new String(translation.getBytes("ISO-8859-1"), "UTF-8"); + } catch(UnsupportedEncodingException e) { + e.printStackTrace(); + } + } else { + return translation; + } + } + return name; + + } + private boolean isOldJava() { + try { + String versionString = System.getProperty("java.class.version"); + Float version = Float.parseFloat(versionString); + if (version < 53) { + return true; + } + } catch (Exception e) { + System.out.println(e.getLocalizedMessage()); + } + return false; + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/RegistrationHandler.classes b/src/main/java/pro/litvinovg/docsettings/RegistrationHandler.classes new file mode 100644 index 0000000..53818d7 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/RegistrationHandler.classes @@ -0,0 +1 @@ +pro.litvinovg.docsettings.DocSettingsExtension \ No newline at end of file diff --git a/src/main/java/pro/litvinovg/docsettings/RegistrationHandler.java b/src/main/java/pro/litvinovg/docsettings/RegistrationHandler.java new file mode 100644 index 0000000..a0adc3e --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/RegistrationHandler.java @@ -0,0 +1,175 @@ +/************************************************************************* + * + * The Contents of this file are made available subject to the terms of + * either of the GNU Lesser General Public License Version 2.1 + * + * Sun Microsystems Inc., October, 2000 + * + * + * GNU Lesser General Public License Version 2.1 + * ============================================= + * Copyright 2000 by Sun Microsystems, Inc. + * 901 San Antonio Road, Palo Alto, CA 94303, USA + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1, as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, + * MA 02111-1307 USA + * + * The Initial Developer of the Original Code is: Sun Microsystems, Inc.. + * + * Copyright: 2002 by Sun Microsystems, Inc. + * + * All Rights Reserved. + * + * Contributor(s): Cedric Bosdonnat + * + * + ************************************************************************/ +package pro.litvinovg.docsettings; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import com.sun.star.lang.XSingleComponentFactory; +import com.sun.star.registry.XRegistryKey; + +/** + * Component main registration class. + * + *

This class should not be modified.

+ * + * @author Cedric Bosdonnat aka. cedricbosdo + * + */ +public class RegistrationHandler { + + /** + * Get a component factory for the implementations handled by this class. + * + *

This method calls all the methods of the same name from the classes listed + * in the RegistrationHandler.classes file. This method + * should not be modified.

+ * + * @param pImplementationName the name of the implementation to create. + * + * @return the factory which can create the implementation. + */ + public static XSingleComponentFactory __getComponentFactory(String sImplementationName ) { + XSingleComponentFactory xFactory = null; + + Class[] classes = findServicesImplementationClasses(); + + int i = 0; + while (i < classes.length && xFactory == null) { + Class clazz = classes[i]; + if ( sImplementationName.equals( clazz.getCanonicalName() ) ) { + try { + Class[] getTypes = new Class[]{String.class}; + Method getFactoryMethod = clazz.getMethod("__getComponentFactory", getTypes); + Object o = getFactoryMethod.invoke(null, sImplementationName); + xFactory = (XSingleComponentFactory)o; + } catch (Exception e) { + // Nothing to do: skip + System.err.println("Error happened"); + e.printStackTrace(); + } + } + i++; + } + return xFactory; + } + + /** + * Writes the services implementation informations to the UNO registry. + * + *

This method calls all the methods of the same name from the classes listed + * in the RegistrationHandler.classes file. This method + * should not be modified.

+ * + * @param pRegistryKey the root registry key where to write the informations. + * + * @return true if the informations have been successfully written + * to the registry key, false otherwise. + */ + public static boolean __writeRegistryServiceInfo(XRegistryKey xRegistryKey ) { + + Class[] classes = findServicesImplementationClasses(); + + boolean success = true; + int i = 0; + while (i < classes.length && success) { + Class clazz = classes[i]; + try { + Class[] writeTypes = new Class[]{XRegistryKey.class}; + Method getFactoryMethod = clazz.getMethod("__writeRegistryServiceInfo", writeTypes); + Object o = getFactoryMethod.invoke(null, xRegistryKey); + success = success && ((Boolean)o).booleanValue(); + } catch (Exception e) { + success = false; + e.printStackTrace(); + } + i++; + } + return success; + } + + /** + * @return all the UNO implementation classes. + */ + private static Class[] findServicesImplementationClasses() { + + ArrayList classes = new ArrayList(); + + InputStream in = RegistrationHandler.class.getResourceAsStream("RegistrationHandler.classes"); + LineNumberReader reader = new LineNumberReader(new InputStreamReader(in)); + + try { + String line = reader.readLine(); + while (line != null) { + if (!line.equals("")) { + line = line.trim(); + try { + Class clazz = Class.forName(line); + + Class[] writeTypes = new Class[]{XRegistryKey.class}; + Class[] getTypes = new Class[]{String.class}; + + Method writeRegMethod = clazz.getMethod("__writeRegistryServiceInfo", writeTypes); + Method getFactoryMethod = clazz.getMethod("__getComponentFactory", getTypes); + + if (writeRegMethod != null && getFactoryMethod != null) { + classes.add(clazz); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + line = reader.readLine(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + reader.close(); + in.close(); + } catch (Exception e) {}; + } + + return classes.toArray(new Class[classes.size()]); + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/StandaloneApplication.java b/src/main/java/pro/litvinovg/docsettings/StandaloneApplication.java new file mode 100644 index 0000000..ca4bc98 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/StandaloneApplication.java @@ -0,0 +1,27 @@ +package pro.litvinovg.docsettings; + +import java.io.File; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.litvinovg.docsettings.document.ODFDocument; +import pro.litvinovg.docsettings.settings.Backups; + +public class StandaloneApplication { + private static final Logger logger = LoggerFactory.getLogger(StandaloneApplication.class); + + public static void main(String args[]) { + if (args.length == 0 ) { + logger.debug("Error. No arguments found. Exit."); + return; + } + final String filePath = args[0]; + File file = new File(filePath); + if (!file.exists()) { + logger.debug("Error. File doesn't exist. Exit."); + return; + } + + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/document/ODFDocument.java b/src/main/java/pro/litvinovg/docsettings/document/ODFDocument.java new file mode 100644 index 0000000..4494a42 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/document/ODFDocument.java @@ -0,0 +1,357 @@ +package pro.litvinovg.docsettings.document; + +import static pro.litvinovg.docsettings.settings.StorageUtils.getDocFromString; +import static pro.litvinovg.docsettings.settings.StorageUtils.getSettingsElement; + +import java.awt.EventQueue; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.apache.tika.Tika; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.sun.star.beans.PropertyValue; +import com.sun.star.beans.XPropertyContainer; +import com.sun.star.beans.XPropertySet; +import com.sun.star.beans.XPropertySetInfo; +import com.sun.star.chart.TimeUnit; +import com.sun.star.document.XDocumentProperties; +import com.sun.star.document.XDocumentPropertiesSupplier; +import com.sun.star.frame.XComponentLoader; +import com.sun.star.frame.XDesktop; +import com.sun.star.frame.XDispatchHelper; +import com.sun.star.frame.XDispatchProvider; +import com.sun.star.frame.XModel; +import com.sun.star.frame.XStorable; +import com.sun.star.lang.XComponent; +import com.sun.star.lang.XMultiComponentFactory; +import com.sun.star.lang.XServiceInfo; +import com.sun.star.text.XTextDocument; +import com.sun.star.uno.UnoRuntime; +import com.sun.star.uno.XComponentContext; +import com.sun.star.util.XModifiable; + +import pro.litvinovg.docsettings.DialogHelper; +import pro.litvinovg.docsettings.Localizer; +import pro.litvinovg.docsettings.gui.MainWindow; +import pro.litvinovg.docsettings.settings.Backups; +import pro.litvinovg.docsettings.settings.ODFSetting; +import pro.litvinovg.docsettings.settings.SettingFactory; +import pro.litvinovg.docsettings.settings.Settings; +import pro.litvinovg.docsettings.settings.SettingsComparator; + +public class ODFDocument { + + + private static final Logger logger = LoggerFactory.getLogger(ODFDocument.class); + + private XComponentContext context = null; + private XDesktop xDesktop = null; + private XMultiComponentFactory multiComponentFactory = null; + private XComponent currentDocument = null; + private String fileName = ""; + public static final String SETTINGS = "settings.xml"; + public static final String META = "meta.xml"; + private Settings currentSettings = null; + + private XStorable storable = null; + + private XComponentLoader loader = null; + + private String url = null; + + public Settings getCurrentSettings() { + if (currentSettings.getSize() == 0) { + readSettings(); + } + return currentSettings; + } + + private String getSettingsContent() { + return readInternalFile(SETTINGS); + } + + private void setSettingsContent(String settingsContent) { + writeInternalFile(SETTINGS, settingsContent); + } + + private String getMetaContent() { + return readInternalFile(META); + } + + /* + * private void setMetaContent(String newMetaContent) { writeInternalFile(META, + * newMetaContent); } + */ + + public ODFDocument(XComponentContext componentContext) throws Exception { + this.currentSettings = new Settings(); + if (componentContext != null) { + context = componentContext; + multiComponentFactory = context.getServiceManager(); + + Object oDesktop = multiComponentFactory.createInstanceWithContext("com.sun.star.frame.Desktop", context); + xDesktop = UnoRuntime.queryInterface(XDesktop.class, oDesktop); + currentDocument = xDesktop.getCurrentComponent(); + if (currentDocument == null) { + logger.debug("xDesktop.getCurrentComponent() returned null"); + throw new Exception(Localizer.get("error_document_is_null")); + } + XModel model = UnoRuntime.queryInterface(XModel.class, currentDocument); + if (!isSupported(currentDocument, "com.sun.star.text.TextDocument")) { + throw new Exception(Localizer.get("error_document_type_is_not_supported")); + } + loader = UnoRuntime.queryInterface(XComponentLoader.class, xDesktop); + XTextDocument textDoc = UnoRuntime.queryInterface(XTextDocument.class, currentDocument); + if (textDoc == null) { + throw new Exception("Error. TextDoc is null."); + } + storable = UnoRuntime.queryInterface(XStorable.class, textDoc); + + if (storable == null || storable.isReadonly()) { + throw new Exception(Localizer.get("error_document_is_ro")); + } + if (!storable.hasLocation() || storable.getLocation().isEmpty()) { + throw new Exception(Localizer.get("error_document_is_not_saved")); + } + + url = model.getURL(); + URI uri = new URI(url); + File file = new File(uri); + if (!file.exists()) { + throw new Exception(Localizer.get("error_document_is_null")); + } + if (!file.canRead()) { + throw new Exception(Localizer.get("error_document_is_ro")); + } + if (!"application/vnd.oasis.opendocument.text".equals(getMimeType(file))) { + throw new Exception(Localizer.get("error_document_type_is_not_supported")); + } + fileName = file.getAbsolutePath(); + } + } + + private String getMimeType(File file) { + Tika tika = new Tika(); + InputStream is; + String mime = ""; + try { + is = new FileInputStream(file); + mime = tika.detect(is); + } catch (IOException e) { + logger.debug("Ops", e); + } + return mime; + } + + public void restoreSettings(Settings settings) { + closeDocument(); + restoreFrom(settings); + reOpenDoc(); + } + + public XPropertyContainer getUserDefinedProps(){ + XDocumentPropertiesSupplier docPropertiesSupplier = UnoRuntime.queryInterface(XDocumentPropertiesSupplier.class, currentDocument); + XDocumentProperties docProperties = docPropertiesSupplier.getDocumentProperties(); + XPropertyContainer userDefinedProperties = docProperties.getUserDefinedProperties(); + return userDefinedProperties; + } + + public void readSettings() { + flushSettings(); + try { + Node configSettings = getSettingsElement(getDocFromString(getSettingsContent())); + NodeList settingNodes = configSettings.getChildNodes(); + for (int i = 0; i < settingNodes.getLength(); i++) { + Node settingNode = settingNodes.item(i); + ODFSetting setting = SettingFactory.create(settingNode); + currentSettings.add(setting); + } + } catch (Exception e) { + logger.debug(e.getMessage()); + logger.debug("Ops!",e); + } + } + + private void flushSettings() { + currentSettings.clear(); + } + + public void restoreFrom(Settings settings) { + String newSettingsContent; + try { + newSettingsContent = settings.toSettingsContent(getSettingsContent()); + writeInternalFile(SETTINGS, newSettingsContent); + + } catch (Exception e) { + logger.debug("Restore failed", e); + } + } + + public void readBackups(Backups backups) { + backups.readBackupsFromMetadata(getUserDefinedProps()); + } + + public boolean hasBackups(Backups backups) { + return !backups.isEmpty(); + } + + public void writeBackups(Backups backups) { + backups.writeBackupsToMetadata(getUserDefinedProps()); + } + + public String getFileName() { + return fileName; + } + + public void printText(String text) { + DialogHelper.showErrorMessage(context, null, text); + } + + public void printFileName() { + DialogHelper.showErrorMessage(context, null, fileName); + } + + private String readInternalFile(String internalFile) { + Map env = new HashMap<>(); + String result = ""; + FileSystem zipfs = null; + env.put("create", "true"); + Path path = Paths.get(fileName); + URI uri = URI.create("jar:" + path.toUri()); + try { + zipfs = FileSystems.newFileSystem(uri, env); + Path settingsPath = zipfs.getPath(internalFile); + byte[] encoded = Files.readAllBytes(settingsPath); + result = new String(encoded, "UTF-8"); + } catch (IOException e) { + logger.debug(e.getMessage()); + logger.debug("Ops!",e); + e.printStackTrace(); + } finally { + try { + zipfs.close(); + } catch (IOException e) { + logger.debug("Ops", e); + e.printStackTrace(); + } + } + return result; + } + + private void writeInternalFile(String internalFile, String contents) { + Map env = new HashMap<>(); + BufferedWriter writer = null; + FileSystem zipfs = null; + env.put("create", "true"); + Path path = Paths.get(fileName); + URI uri = URI.create("jar:" + path.toUri()); + try { + zipfs = FileSystems.newFileSystem(uri, env); + Path internalPath = zipfs.getPath(internalFile); + writer = Files.newBufferedWriter(internalPath, StandardCharsets.UTF_8, StandardOpenOption.WRITE); + writer.write(contents); + } catch (IOException e) { + logger.debug(e.getMessage()); + logger.debug("Ops!",e); + } finally { + try { + writer.close(); + zipfs.close(); + } catch (IOException e) { + logger.debug(e.getLocalizedMessage()); + logger.debug("Ops!",e); + } + } + } + + + public void reOpenDoc(){ + try { + XComponent doc = loader.loadComponentFromURL(url, "_default", 0, new PropertyValue[0]); + } catch (Exception e) { + System.out.println("Failed to open the document"); + } + ; + } + + public void closeDocument() { + if (context != null) { + executedispatch("CloseDoc", new PropertyValue[0]); + } else { + logger.debug("Can't close document. No context."); + } + } + + public void reloadDocument() { + if (context != null) { + executedispatch("Reload", new PropertyValue[0]); + } else { + logger.debug("Can't reload document. No context."); + } + } + + private boolean isSupported(Object object, String service) { + XServiceInfo info = UnoRuntime.queryInterface(XServiceInfo.class, object); + if (info == null) { + return false; + } + return info.supportsService(service); + } + + private boolean executedispatch( String cmd, PropertyValue[] props) { + XDispatchHelper helper = createInstance(XDispatchHelper.class, "com.sun.star.frame.DispatchHelper"); + if (helper == null) { + logger.debug("Could not create dispatch helper for command " + cmd); + return false; + } + try { + Object oDesktop = context.getServiceManager().createInstanceWithContext("com.sun.star.frame.Desktop", context); + XDesktop xDesktop = UnoRuntime.queryInterface(XDesktop.class, oDesktop); + XDispatchProvider provider = UnoRuntime.queryInterface(XDispatchProvider.class, xDesktop.getCurrentFrame()); + helper.executeDispatch(provider, ".uno:" + cmd, "", 0, props); + return true; + } catch (java.lang.Exception e) { + logger.debug("Could not dispatch '" + cmd + "':\n " + e); + } + return false; + } + + private T createInstance(Class aType, String serviceName) { + if ((context == null)) { + logger.debug("No libreoffice context. Cant create instance of " + aType.toString()); + return null; + } + T interfaceObj = null; + try { + Object o = context.getServiceManager().createInstanceWithContext(serviceName, context); + interfaceObj = UnoRuntime.queryInterface(aType, o); + } catch (Exception e) { + logger.debug("Couldn't create interface for '" + serviceName + "': " + e); + } + return interfaceObj; + } + + public boolean compareWithBackup(Settings backedUpSettings) { + return SettingsComparator.isEqual(getCurrentSettings(), backedUpSettings); + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/exceptions/MalformedXmlException.java b/src/main/java/pro/litvinovg/docsettings/exceptions/MalformedXmlException.java new file mode 100644 index 0000000..c633172 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/exceptions/MalformedXmlException.java @@ -0,0 +1,10 @@ +package pro.litvinovg.docsettings.exceptions; + +public class MalformedXmlException extends Exception{ + + private static final long serialVersionUID = 1L; + + public MalformedXmlException(String message) { + super(message); + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/gui/BackupPanel.java b/src/main/java/pro/litvinovg/docsettings/gui/BackupPanel.java new file mode 100644 index 0000000..89d4107 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/gui/BackupPanel.java @@ -0,0 +1,202 @@ +package pro.litvinovg.docsettings.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultListModel; +import javax.swing.DefaultListSelectionModel; +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Alignment; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.LayoutStyle.ComponentPlacement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.litvinovg.docsettings.ExtensionConfig; +import pro.litvinovg.docsettings.Localizer; +import pro.litvinovg.docsettings.document.ODFDocument; +import pro.litvinovg.docsettings.settings.Backups; +import pro.litvinovg.docsettings.settings.Settings; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.SwingConstants; + +public class BackupPanel extends JPanel { + + private ExtensionConfig config; + private ODFDocument document; + private Backups backups; + private JButton btn_close; + private JButton btn_restore; + private JButton btn_create; + private JButton btn_delete; + private JList lst_backups; + private DefaultListModel backupListModel; + private static final Logger logger = LoggerFactory.getLogger(BackupPanel.class); + private JCheckBox cb_stick_latest_backup; + private JFrame frame; + + + public BackupPanel(JFrame frame, ODFDocument document, Backups backups) { + this.frame = frame; + this.backups = backups; + this.config = ExtensionConfig.getInstance(); + this.document = document; + initComponents(); + initEvents(); + } + + private void initEvents() { + onCreateButtonClick(); + onDeleteButtonClick(); + onRestoreButtonClick(); + onCloseButtonClick(); + onCheckBoxClick(); + } + + private void initComponents() { + + backupListModel = new DefaultListModel(); + populateList(); + lst_backups = new JList(backupListModel); + lst_backups.setSelectionMode(DefaultListSelectionModel.SINGLE_SELECTION); + + JLabel lb_backups = new JLabel(Localizer.get("backup_panel_description")); + + btn_delete = new JButton(Localizer.get("delete_backup_button")); + btn_create = new JButton(Localizer.get("create_backup_button")); + btn_restore = new JButton(Localizer.get("restore_button")); + btn_close = new JButton(Localizer.get("close_window_button")); + + cb_stick_latest_backup = new JCheckBox(Localizer.get("stick_latest_backup_cb_label")); + cb_stick_latest_backup.setSelected(config.getAutoRestore()); + cb_stick_latest_backup.setVerticalAlignment(SwingConstants.TOP); + + GroupLayout groupLayout = new GroupLayout(this); + groupLayout.setHorizontalGroup( + groupLayout.createParallelGroup(Alignment.LEADING) + .addGroup(groupLayout.createSequentialGroup() + .addGap(43) + .addGroup(groupLayout.createParallelGroup(Alignment.LEADING, false) + .addComponent(lb_backups, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(groupLayout.createSequentialGroup() + .addComponent(lst_backups, GroupLayout.PREFERRED_SIZE, 354, GroupLayout.PREFERRED_SIZE) + .addGap(26) + .addGroup(groupLayout.createParallelGroup(Alignment.LEADING, false) + .addComponent(cb_stick_latest_backup, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btn_close, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btn_delete, Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, 269, Short.MAX_VALUE) + .addComponent(btn_create, Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btn_restore, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + .addContainerGap(288, Short.MAX_VALUE)) + ); + groupLayout.setVerticalGroup( + groupLayout.createParallelGroup(Alignment.LEADING) + .addGroup(groupLayout.createSequentialGroup() + .addContainerGap() + .addComponent(lb_backups, GroupLayout.PREFERRED_SIZE, 15, GroupLayout.PREFERRED_SIZE) + .addGap(14) + .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) + .addComponent(lst_backups, GroupLayout.PREFERRED_SIZE, 384, GroupLayout.PREFERRED_SIZE) + .addGroup(groupLayout.createSequentialGroup() + .addComponent(btn_delete) + .addGap(18) + .addComponent(btn_create) + .addGap(18) + .addComponent(btn_restore) + .addGap(33) + .addComponent(cb_stick_latest_backup, GroupLayout.PREFERRED_SIZE, 83, GroupLayout.PREFERRED_SIZE) + .addGap(132) + .addComponent(btn_close))) + .addContainerGap(74, Short.MAX_VALUE)) + ); + setLayout(groupLayout); + } + + private void populateList() { + backupListModel.clear(); + int i = 0; + for(String date : backups.getBackupTimeLabels()) { + backupListModel.add(i,date); + i++; + } + } + + private void onCreateButtonClick() { + btn_create.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if (document == null) { + return; + } + backups.create(document.getCurrentSettings()); + document.writeBackups(backups); + populateList(); + } + }); + } + + private void onDeleteButtonClick() { + btn_delete.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if (lst_backups.isSelectionEmpty()) { + return; + } + if (document == null) { + return; + } + String selected = (String) lst_backups.getSelectedValue(); + backups.removeByDateTimeLabel(selected); + document.writeBackups(backups); + populateList(); + } + }); + } + + private void onRestoreButtonClick() { + btn_restore.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if (lst_backups.isSelectionEmpty()) { + JOptionPane.showMessageDialog(frame, Localizer.get("no_backup_selected")); + return; + } + if (document == null) { + JOptionPane.showMessageDialog(frame, Localizer.get("error_document_not_found")); + return; + } + String selected = (String) lst_backups.getSelectedValue(); + Settings backup; + try { + backup = backups.getByDateTimeLabel(selected); + document.restoreSettings(backup); + MainWindow.getSingleFrame().setVisible(false); + MainWindow.getSingleFrame().dispose(); + } catch (Exception e) { + logger.debug("Restore failed", e); + JOptionPane.showMessageDialog(frame, Localizer.get("restore_failed \n" + e.getLocalizedMessage())); + } + } + }); + } + + private void onCloseButtonClick() { + btn_close.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + MainWindow.getSingleFrame().setVisible(false); + MainWindow.getSingleFrame().dispose(); + } + }); + } + + private void onCheckBoxClick() { + cb_stick_latest_backup.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + config.setAutoRestore(cb_stick_latest_backup.isSelected()); + } + }); + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/gui/ChangedSettings.java b/src/main/java/pro/litvinovg/docsettings/gui/ChangedSettings.java new file mode 100644 index 0000000..75013ba --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/gui/ChangedSettings.java @@ -0,0 +1,42 @@ +package pro.litvinovg.docsettings.gui; + +import java.awt.BorderLayout; +import java.awt.EventQueue; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; + +public class ChangedSettings extends JFrame { + + private JPanel contentPane; + + /** + * Launch the application. + */ + public static void main(String[] args) { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + ChangedSettings frame = new ChangedSettings(); + frame.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /** + * Create the frame. + */ + public ChangedSettings() { + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setBounds(100, 100, 450, 300); + contentPane = new JPanel(); + contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); + contentPane.setLayout(new BorderLayout(0, 0)); + setContentPane(contentPane); + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/gui/MainWindow.java b/src/main/java/pro/litvinovg/docsettings/gui/MainWindow.java new file mode 100644 index 0000000..736a21e --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/gui/MainWindow.java @@ -0,0 +1,123 @@ +package pro.litvinovg.docsettings.gui; + +import java.awt.EventQueue; +import java.awt.Toolkit; + +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +import java.awt.GridLayout; +import javax.swing.JTabbedPane; +import javax.swing.UIManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.star.uno.XComponentContext; + +import pro.litvinovg.docsettings.DialogHelper; +import pro.litvinovg.docsettings.DocSettingsExtension; +import pro.litvinovg.docsettings.ExtensionConfig; +import pro.litvinovg.docsettings.Localizer; +import pro.litvinovg.docsettings.document.ODFDocument; +import pro.litvinovg.docsettings.settings.Backups; + +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import javax.swing.BoxLayout; +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Alignment; + +import javax.swing.JList; + +public class MainWindow extends JFrame { + + private static final long serialVersionUID = 1L; + private static JFrame frame = null; + private Localizer localizer; + private ODFDocument document; + private Backups backups; + private static final Logger logger = LoggerFactory.getLogger(MainWindow.class); + + + public static void main(String[] args) { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + MainWindow window = new MainWindow(); + frame = window; + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + public static void runGUI(XComponentContext context) { + + if (frame != null) { + frame.dispose(); + } + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Throwable e) { + String message = e.getLocalizedMessage(); + JOptionPane.showMessageDialog(frame, message); + } + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + MainWindow window = new MainWindow(context); + frame = window; + } catch (Throwable e) { + DialogHelper.showErrorMessage(context, null, e.getLocalizedMessage()); + logger.debug("Ops", e); + } + } + }); + + } + + public static JFrame getSingleFrame() { + return frame; + } + + public MainWindow() { + backups = new Backups(); + initialize(); + } + + public MainWindow(XComponentContext context) throws Exception { + document = new ODFDocument(context); + backups = new Backups(); + document.readSettings(); + document.readBackups(backups); + initialize(); + } + + /** + * Initialize the contents of the frame. + */ + private void initialize() { + setIconImage(Toolkit.getDefaultToolkit().getImage(MainWindow.class.getResource("/pro/litvinovg/docsettings/resources/icon.png"))); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + getContentPane().setLayout(new GridLayout(1, 0, 0, 0)); + localizer = Localizer.getInstance(); + this.setBounds(100, 100, 750, 500); + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + this.setVisible(true); + + JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); + getContentPane().add(tabbedPane); + + JPanel backupPanel = new BackupPanel(this, document, backups); + tabbedPane.addTab(localizer.getTranslation("backups_tab_name"), null, backupPanel, null); + + //JPanel settingsPanel = new SettingsPanel(this, document); + //tabbedPane.addTab(localizer.getTranslation("settings_tab_name"), null, settingsPanel, null); + + + + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/gui/SettingsPanel.java b/src/main/java/pro/litvinovg/docsettings/gui/SettingsPanel.java new file mode 100644 index 0000000..49eff11 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/gui/SettingsPanel.java @@ -0,0 +1,33 @@ +package pro.litvinovg.docsettings.gui; + +import javax.swing.JPanel; + +import pro.litvinovg.docsettings.ExtensionConfig; +import pro.litvinovg.docsettings.Localizer; +import pro.litvinovg.docsettings.document.ODFDocument; +import java.awt.GridLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JComboBox; +import javax.swing.JFrame; + +public class SettingsPanel extends JPanel { + + private ExtensionConfig config; + private Localizer localizer; + private ODFDocument document; + private JFrame frame; + + public SettingsPanel(JFrame mainWindow, ODFDocument document) { + this.frame = mainWindow; + this.document = document; + this.config = ExtensionConfig.getInstance(); + this.localizer = Localizer.getInstance(); + setLayout(new GridLayout(1, 0, 0, 0)); + initComponents(); + } + + private void initComponents() { + + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/localizations/docsettings.properties b/src/main/java/pro/litvinovg/docsettings/localizations/docsettings.properties new file mode 100644 index 0000000..c548a78 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/localizations/docsettings.properties @@ -0,0 +1,15 @@ +close_window_button = Close +restore_button = Restore from backup +create_backup_button = Create backup +delete_backup_button = Delete backup +settings_tab_name = Settings +backups_tab_name = Backups +backup_panel_description = Manage document settings backups +no_timestamp_found_error = No backup dates found +stick_latest_backup_cb_label = Block settings +no_backup_selected = Select backup and try again +error_document_not_found = Document not found. +error_document_is_not_saved = Document is not saved. Save document before proceeding. +error_document_is_ro = Document opened in read only mode. +error_document_is_null = Document not found +error_document_type_is_not_supported = Document type is not supported. \ No newline at end of file diff --git a/src/main/java/pro/litvinovg/docsettings/localizations/docsettings_ru.properties b/src/main/java/pro/litvinovg/docsettings/localizations/docsettings_ru.properties new file mode 100644 index 0000000..9561147 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/localizations/docsettings_ru.properties @@ -0,0 +1,15 @@ +close_window_button = Закрыть +restore_button = Восстановить из резервной копии +create_backup_button = Создать резервную копию +delete_backup_button = Удалить резервную копию +settings_tab_name = Настройки +backups_tab_name = Резервные копии +backup_panel_description = Управление резервными копиями настроек документов +no_timestamp_found_error = Не найдено дат резервных копий +stick_latest_backup_cb_label = Заблокировать изменение настроек +no_backup_selected = Выберите резервную копию и попробуйте снова +error_document_not_found = Ошибка. Документ не найден. +error_document_is_not_saved = Документ не сохранен. Сохраните документ и попробуйте снова. +error_document_is_ro = Документ открыт в режиме только для чтения. +error_document_is_null = Ошибка. Документ не найден. +error_document_type_is_not_supported = Тип документа не поддерживается. Сохраните документ в формате ODT. \ No newline at end of file diff --git a/src/main/java/pro/litvinovg/docsettings/resources/icon.png b/src/main/java/pro/litvinovg/docsettings/resources/icon.png new file mode 100644 index 0000000..e89adc0 Binary files /dev/null and b/src/main/java/pro/litvinovg/docsettings/resources/icon.png differ diff --git a/src/main/java/pro/litvinovg/docsettings/settings/Backups.java b/src/main/java/pro/litvinovg/docsettings/settings/Backups.java new file mode 100644 index 0000000..fcd87b9 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/Backups.java @@ -0,0 +1,445 @@ +package pro.litvinovg.docsettings.settings; + +import static pro.litvinovg.docsettings.settings.StorageUtils.*; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeMap; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import com.sun.star.beans.IllegalTypeException; +import com.sun.star.beans.Property; +import com.sun.star.beans.PropertyExistException; +import com.sun.star.beans.UnknownPropertyException; +import com.sun.star.beans.XPropertyContainer; +import com.sun.star.beans.XPropertySet; +import com.sun.star.beans.XPropertySetInfo; +import com.sun.star.lang.IllegalArgumentException; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.uno.UnoRuntime; + +import pro.litvinovg.docsettings.exceptions.MalformedXmlException; + +public class Backups { + + private static final short REMOVABLE_PROPERTY = 128; + private static final int OO_XML_MAX_LENGTH = 65535; + private static final Logger logger = LoggerFactory.getLogger(Backups.class); + private static final String BACKUP_NAME_PREFIX = "SerializedSettings-"; + private TreeMap backups = null; + + public Backups() { + backups = new TreeMap(); + } + + public void add(Long unixTimeStamp, Settings backup) { + backups.put(unixTimeStamp, backup); + } + public void clear() { + backups.clear(); + } + public boolean isEmpty() { + return backups.isEmpty(); + } + + public void create(Settings currentSettings) { + backups.put( getUnixTime(), currentSettings); + } + + public Settings getBackupByTimeStamp(Long timeStamp) { + return backups.get(timeStamp); + } + + public Settings getLatest() { + return backups.lastEntry().getValue(); + } + + public Set getTimeStamps() { + return backups.keySet(); + } + + public void readBackupsFromMetadata(XPropertyContainer userDefinedProps) { + clear(); + TreeMap> backupMap = getBackupMap(userDefinedProps); + Map serializedBackups = combineBackupStrings(backupMap); + for (Entry entry : serializedBackups.entrySet()) { + Long unixTimeStamp = entry.getKey(); + String backupString = entry.getValue(); + if (!backupString.isEmpty()) { + backupString = decodeBase64(backupString); + readBackup(unixTimeStamp, backupString); + } + } + } + + public void writeBackupsToMetadata(XPropertyContainer userDefinedProps) { + cleanBackupMetadata(userDefinedProps); + for (Entry backup : backups.entrySet()) { + try { + addBackupEntries( userDefinedProps, backup); + } catch (MalformedXmlException | ParserConfigurationException | TransformerException e) { + logger.debug("Ops", e); + } + } + } + + private void cleanBackupMetadata(XPropertyContainer userDefinedProps) { + XPropertySet propertySet = UnoRuntime.queryInterface(XPropertySet.class, userDefinedProps); + XPropertySetInfo propSetInfo = propertySet.getPropertySetInfo(); + Property[] props = propSetInfo.getProperties(); + for (int i = 0; i < props.length; i++) { + try { + String name = props[i].Name; + if (name.startsWith(BACKUP_NAME_PREFIX)) { + userDefinedProps.removeProperty(name); + } + } catch (Exception e) { + logger.debug("Ops", e); + } + } + } + + private void addBackupEntries(XPropertyContainer userDefinedProps, Entry backup) throws MalformedXmlException, ParserConfigurationException, TransformerException { + String backupString = serializeSettingsXML(backup.getValue()); + backupString = encodeBase64(backupString); + List parts = splitByOOMaxLength(backupString); + for (int i = 0; i < parts.size();i++) { + String part = parts.get(i); + String name = formatBackupMetaName(backup.getKey(),i); + try { + userDefinedProps.addProperty(name, REMOVABLE_PROPERTY, part); + } catch (IllegalArgumentException e) { + logger.debug("Ops", e); + } catch (PropertyExistException e) { + //it is ok + logger.debug("Ops", e); + } catch (IllegalTypeException e) { + logger.debug("Ops", e); + } + + } + } + + private void readBackup(Long unixTimeStamp, String backupString) { + Document backupDoc; + try { + backupDoc = getDocFromString(backupString); + Element configSettings = (Element) getSettingsElement(backupDoc); + NodeList settingNodes = configSettings.getChildNodes(); + Settings backup = readSettingsFromNodeList(settingNodes); + add(unixTimeStamp,backup); + } catch (ParserConfigurationException | SAXException | IOException e) { + logger.debug("Ops", e); + } catch (Exception e) { + logger.debug("Ops", e); + } + } + + private TreeMap> getBackupMap(XPropertyContainer userDefinedProps) { + TreeMap> backupMap = new TreeMap>(); + + XPropertySet propertySet = UnoRuntime.queryInterface(XPropertySet.class, userDefinedProps); + XPropertySetInfo propSetInfo = propertySet.getPropertySetInfo(); + Property[] props = propSetInfo.getProperties(); + for (int i = 0; i < props.length; i++) { + try { + Property prop = props[i]; + String name = prop.Name; + if (name.startsWith(BACKUP_NAME_PREFIX)) { + Long timeStamp = Long.parseLong(getTimeStampFromString(name)); + Long order = Long.parseLong(getOrderFromString(name)); + String stringPart = propertySet.getPropertyValue(prop.Name).toString(); + Map backupParts = null; + if (backupMap.containsKey(timeStamp)) { + backupParts = backupMap.get(timeStamp); + } else { + backupParts = new TreeMap(); + backupMap.put(timeStamp, backupParts); + } + backupParts.put(order, stringPart); + } + //logger.debug("Prop name "+ prop.Name + " Prop value " + propertySet.getPropertyValue(prop.Name)); + } catch (UnknownPropertyException | WrappedTargetException e) { + logger.debug("Ops", e); + } + } + return backupMap; + } + + public void readBackupsFromXML(String metaContent) { + clear(); + try { + Document doc = getDocFromString(metaContent); + Map serializedBackups = getSerializedBackups(doc); + for (Entry entry : serializedBackups.entrySet()) { + Long unixTimeStamp = entry.getKey(); + String backupString = entry.getValue(); + if (!backupString.isEmpty()) { + backupString = decodeBase64(backupString); + Document backupDoc = getDocFromString(backupString); + Element configSettings = (Element) getSettingsElement(backupDoc); + NodeList settingNodes = configSettings.getChildNodes(); + Settings backup = readSettingsFromNodeList(settingNodes); + add(unixTimeStamp,backup); + } + } + } catch (Exception e) { + logger.debug(e.getMessage()); + logger.debug("Ops!",e); + } + } + + private Settings readSettingsFromNodeList(NodeList settingNodes) throws MalformedXmlException { + Settings settings = new Settings(); + for (int i = 0; i < settingNodes.getLength(); i++) { + Node settingNode = settingNodes.item(i); + ODFSetting setting = SettingFactory.create(settingNode); + settings.add(setting); + } + return settings; + } + + public String getNewMetaContent(String metaContent) { + String cleanedMeta = getCleanedMetaContent(metaContent); + String newMetaContent = ""; + try { + Document doc = getDocFromString(cleanedMeta); + for (Entry backup : backups.entrySet()) { + addXMLBackupEntries( doc, backup.getKey(), backup.getValue()); + } + newMetaContent = serializeDoc(doc); + } catch (Exception e) { + logger.debug(e.getMessage()); + logger.debug("Ops!",e); + } + return newMetaContent; + } + + + + private void addXMLBackupEntries(Document doc, Long timeStamp, Settings settings) throws MalformedXmlException, ParserConfigurationException, TransformerException { + String backupString = serializeSettingsXML(settings); + backupString = encodeBase64(backupString); + List parts = splitByOOMaxLength(backupString); + for (int i = 0; i < parts.size();i++) { + String part = parts.get(i); + addMetadataEntry(doc,timeStamp,part, i); + } + } + + private void addMetadataEntry(Document doc, Long timeStamp, String string, int i) throws MalformedXmlException { + Element element = doc.createElement("meta:user-defined"); + String name = formatBackupMetaName(timeStamp,i); + element.setAttribute("meta:name", name); + element.setTextContent(string); + Node metaRoot = getMetaRootElement(doc); + metaRoot.appendChild(element); + } + + private String formatBackupMetaName(Long timeStamp, int i) { + String result = BACKUP_NAME_PREFIX + timeStamp.toString() + "-" + i; + return result; + } + + private List splitByOOMaxLength(String text) { + List array = new ArrayList(); + int index = 0; + while (index < text.length()) { + array.add(text.substring(index, Math.min(index + OO_XML_MAX_LENGTH, text.length()))); + index += OO_XML_MAX_LENGTH; + } + return array; + } + + private String serializeSettingsXML(Settings settings) throws ParserConfigurationException, TransformerException { + Document doc = createNewDoc(); + Element root = doc.createElementNS("urn:oasis:names:tc:opendocument:xmlns:config:1.0","config:config-item-set"); + root.setAttribute("xmlns:config", "urn:oasis:names:tc:opendocument:xmlns:config:1.0"); + root.setAttribute("xmlns:ooo", "http://openoffice.org/2004/office"); + root.setAttribute("config:name", "ooo:configuration-settings"); + doc.appendChild(root); + appendSettingNodesToDoc(settings, doc, root); + String result = serializeDoc(doc); + return result; + } + + + private String getCleanedMetaContent(String metaContent) { + String cleanedMetaContent = ""; + try { + Document doc = getDocFromString(metaContent); + NodeList nodes = getBackupEntries(doc); + removeNodes(nodes); + cleanedMetaContent = serializeDoc(doc); + } catch (Exception e) { + logger.debug(e.getMessage()); + logger.debug("Ops!",e); + } + return cleanedMetaContent; + } + + + + private Map getSerializedBackups(Document doc) throws MalformedXmlException { + NodeList nodes = getBackupEntries(doc); + TreeMap> backupStringMap = sortBackupStrings(nodes); + Map result = combineBackupStrings(backupStringMap); + return result; + } + + private NodeList getBackupEntries(Document doc) throws MalformedXmlException { + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr; + NodeList nodes; + try { + expr = xpath.compile("//*[local-name() ='user-defined'][@*[local-name() ='name' and starts-with(.,'"+ BACKUP_NAME_PREFIX + "')]]"); + nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + } catch (XPathExpressionException e) { + logger.debug(e.getLocalizedMessage()); + throw new MalformedXmlException("Xpath expression failed while looking for backup entries"); + } + return nodes; + } + + private Map combineBackupStrings(TreeMap> backupMap) { + TreeMap result = new TreeMap(); + for(Map.Entry> entry: backupMap.entrySet()) { + Long timeStamp = entry.getKey(); + Map parts = entry.getValue(); + String combinedString = ""; + for(Map.Entry part: parts.entrySet()) { + String serlializedString = part.getValue(); + combinedString += serlializedString; + } + result.put(timeStamp, combinedString); + } + return result; + } + + private TreeMap> sortBackupStrings(NodeList nodes) throws MalformedXmlException { + TreeMap> backupMap = new TreeMap>(); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + Long timeStamp = getTimeStampFromNode(node); + Long order = getOrderFromNode(node); + String stringPart = node.getTextContent(); + Map backupParts = null; + if (backupMap.containsKey(timeStamp)) { + backupParts = backupMap.get(timeStamp); + } else { + backupParts = new TreeMap(); + backupMap.put(timeStamp, backupParts); + } + backupParts.put(order, stringPart); + } + return backupMap; + } + + private Long getTimeStampFromNode(Node node) throws MalformedXmlException { + String name = node.getAttributes().getNamedItem("meta:name").getNodeValue(); + String ts = getTimeStampFromString(name); + Long timeStamp = null; + try { + timeStamp = Long.parseLong(ts); + } catch (NumberFormatException e) { + throw new MalformedXmlException("timestamp not found for metadata backup entry " + name); + } + return timeStamp; + } + + private String getTimeStampFromString(String name) { + return name.replaceAll(BACKUP_NAME_PREFIX, "").replaceAll("-[0-9]$",""); + } + + private Long getOrderFromNode(Node node) throws MalformedXmlException { + String name = node.getAttributes().getNamedItem("meta:name").getNodeValue(); + String or = getOrderFromString(name); + Long order; + try { + order = Long.parseLong(or); + } catch (NumberFormatException e) { + throw new MalformedXmlException("order not found for metadata backup entry " + name); + } + return order; + } + + private String getOrderFromString(String name) { + return name.replaceAll(BACKUP_NAME_PREFIX + "[0-9]+-", ""); + } + + private long getUnixTime() { + return System.currentTimeMillis() / 1000L; + } + + public String[] getBackupTimeLabels() { + String[] timeLabels = new String[backups.size()]; + int i = 0; + for(Long backupTime: backups.keySet()) { + timeLabels[i] = getDateTimeLabelsFromUnix(backupTime); + i++; + } + return timeLabels; + } + + private String getDateTimeLabelsFromUnix(Long timeStamp){ + ZoneId zoneId = TimeZone.getDefault().toZoneId(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + final Instant instant = Instant.ofEpochSecond(timeStamp); + LocalDateTime time = LocalDateTime.ofInstant(instant, zoneId); + return formatter.format(time); + } + + private Long getUnixFromDateTimeLabels(String formattedTime){ + ZoneId zoneId = TimeZone.getDefault().toZoneId(); + DateTimeFormatter parseFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + LocalDateTime time = LocalDateTime.parse(formattedTime, parseFormat); + return time.atZone(zoneId).toEpochSecond(); + } + + public void removeByDateTimeLabel(String dateTimeLabel) { + if (dateTimeLabel == null || dateTimeLabel.isEmpty()) { + return; + } + Long unix = getUnixFromDateTimeLabels(dateTimeLabel); + backups.remove(unix); + } + + public Settings getByDateTimeLabel(String dateTimeLabel) throws Exception { + if (dateTimeLabel == null || dateTimeLabel.isEmpty()) { + throw new Exception("date time not found"); + } + Long unix = getUnixFromDateTimeLabels(dateTimeLabel); + Settings result = backups.get(unix); + if (result == null) { + throw new Exception("backup not found"); + } + return result; + } + + +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/ComparatorResults.java b/src/main/java/pro/litvinovg/docsettings/settings/ComparatorResults.java new file mode 100644 index 0000000..1eda21c --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/ComparatorResults.java @@ -0,0 +1,66 @@ +package pro.litvinovg.docsettings.settings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ComparatorResults { + + private static final Logger logger = LoggerFactory.getLogger(ComparatorResults.class); + + private List removedSettings = new ArrayList(); + private List newSetting = new ArrayList(); + private Map changedSettings = new HashMap(); + + public boolean hasChanges() { + return removedSettings.isEmpty() && newSetting.isEmpty() && changedSettings.isEmpty(); + } + + public void addRemovedProperty(ODFSetting setting){ + removedSettings.add(setting); + } + + public void addNewProperty(ODFSetting setting){ + newSetting.add(setting); + } + + public void addChangedProperty(ODFSetting oldSetting,ODFSetting newSetting){ + changedSettings.put(oldSetting, newSetting); + } + + public List getRemovedProperties() { + return removedSettings; + } + + public List getAddedProperties() { + return newSetting; + } + + public Map getChangedProperties() { + return changedSettings; + } + + public void log() { + logger.info("New settings:"); + for (Iterator iterator = newSetting.iterator(); iterator.hasNext();) { + ODFSetting odfSetting = (ODFSetting) iterator.next(); + logger.debug(odfSetting.getName()); + } + logger.info("Removed settings:"); + for (Iterator iterator = removedSettings.iterator(); iterator.hasNext();) { + ODFSetting odfSetting = (ODFSetting) iterator.next(); + logger.info(odfSetting.getName()); + } + logger.info("Changed settings:"); + for (Entry entry : changedSettings.entrySet()) { + logger.info(entry.getKey().getName() + "\nChanged from "+ entry.getKey().getValue() + " to " + entry.getValue().getValue() ); + } + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/ODFSetting.java b/src/main/java/pro/litvinovg/docsettings/settings/ODFSetting.java new file mode 100644 index 0000000..00cf036 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/ODFSetting.java @@ -0,0 +1,11 @@ +package pro.litvinovg.docsettings.settings; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +public interface ODFSetting { + public String getName(); + public String getValue(); + public String getType(); + public void addToDocumentElement(Document doc, Node root); +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/ODFSettings.java b/src/main/java/pro/litvinovg/docsettings/settings/ODFSettings.java new file mode 100644 index 0000000..bf9874e --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/ODFSettings.java @@ -0,0 +1,24 @@ +package pro.litvinovg.docsettings.settings; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.star.uno.XComponentContext; + +import pro.litvinovg.docsettings.DialogHelper; + +public class ODFSettings { + + private XComponentContext context; + private static final Logger logger = LoggerFactory.getLogger(ODFSettings.class); + + public ODFSettings(XComponentContext context ) { + this.context = context; + } + + public void printText(String text) { + DialogHelper.showErrorMessage(context, null, text); + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/SettingFactory.java b/src/main/java/pro/litvinovg/docsettings/settings/SettingFactory.java new file mode 100644 index 0000000..17a1a17 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/SettingFactory.java @@ -0,0 +1,64 @@ +package pro.litvinovg.docsettings.settings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Node; + +import pro.litvinovg.docsettings.exceptions.MalformedXmlException; + +import static pro.litvinovg.docsettings.settings.StorageUtils.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import pro.litvinovg.docsettings.exceptions.MalformedXmlException; + +public class SettingFactory { + private static final String TYPE_INT = "int"; + private static final String TYPE_BASE64_BINARY = "base64Binary"; + private static final String TYPE_STRING = "string"; + private static final String TYPE_SHORT = "short"; + private static final String TYPE_BOOLEAN = "boolean"; + private static final Logger logger = LoggerFactory.getLogger(SettingFactory.class); + + public static ODFSetting create(Node node) throws MalformedXmlException { + Node typeNode = node.getAttributes().getNamedItem("config:type"); + if (typeNode == null) { + logger.debug("nodeType is null"); + throw new MalformedXmlException("Node " + node.getNodeName() + " config:type attribute not found."); + } + String type = typeNode.getNodeValue(); + if (type == null || type.isEmpty()) { + logger.debug("node type value is null or empty"); + throw new MalformedXmlException("Node " + node.getNodeName() + " config:type attribute not found."); + } + if (TYPE_BOOLEAN.equals(type)|| + TYPE_SHORT.equals(type)|| + TYPE_STRING.equals(type)|| + TYPE_BASE64_BINARY.equals(type)|| + TYPE_INT.equals(type)) { + return new SimpleSetting(node); + } + logger.debug("node type not recognized"); + throw new MalformedXmlException("Node " + node.getNodeName()); + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/Settings.java b/src/main/java/pro/litvinovg/docsettings/settings/Settings.java new file mode 100644 index 0000000..68b76a8 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/Settings.java @@ -0,0 +1,78 @@ +package pro.litvinovg.docsettings.settings; + +import static pro.litvinovg.docsettings.settings.StorageUtils.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class Settings { + private static final Logger logger = LoggerFactory.getLogger(Settings.class); + + + private List settings = null; + + public Settings() { + this.settings = new ArrayList(); + } + + public void add(ODFSetting setting) { + settings.add(setting); + } + + public void remove(int index) { + settings.remove(index); + } + + public void remove(ODFSetting setting) { + settings.remove(setting); + } + + public int getSize() { + return settings.size(); + } + + public ODFSetting get(int num) { + return settings.get(num); + } + + public ODFSetting getByName(String name) { + for (Iterator iterator = settings.iterator(); iterator.hasNext();) { + ODFSetting odfSetting = (ODFSetting) iterator.next(); + if (name.equals(odfSetting.getName())) { + return odfSetting; + } + } + return null; + } + + public void clear() { + settings.clear(); + } + + public boolean isEmpty() { + return settings.isEmpty(); + } + + public String toSettingsContent(String content) throws Exception { + Document doc = getDocFromString(content); + Node settingsContainer = getSettingsElement(doc); + NodeList settingNodes = settingsContainer.getChildNodes(); + removeNodes(settingNodes); + appendSettingNodesToDoc(this, doc, settingsContainer); + String newSettingsContent = serializeDoc(doc); + return newSettingsContent; + } + + public void addAll(Settings inputSettings) { + for (int i = 0; i < inputSettings.getSize(); i++) { + this.add(inputSettings.get(i)); + } + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/SettingsComparator.java b/src/main/java/pro/litvinovg/docsettings/settings/SettingsComparator.java new file mode 100644 index 0000000..ee2331d --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/SettingsComparator.java @@ -0,0 +1,48 @@ +package pro.litvinovg.docsettings.settings; + +public class SettingsComparator { + + private static final String RSID = "Rsid"; + + public static boolean isEqual(Settings currentSettings, Settings backedUpSettings) { + Settings newSettings = new Settings(); + newSettings.addAll(currentSettings); + Settings oldSettings = new Settings(); + oldSettings.addAll(backedUpSettings); + ComparatorResults results = compare(newSettings, oldSettings); + if (results.hasChanges()) { + results.log(); + } + return !results.hasChanges(); + } + + private static ComparatorResults compare(Settings newSettings, Settings oldSettings) { + ComparatorResults results = new ComparatorResults(); + while (!newSettings.isEmpty()) { + ODFSetting newSet = newSettings.get(0); + ODFSetting oldSet = oldSettings.getByName(newSet.getName()); + if (oldSet == null) { + results.addNewProperty(newSet); + } else { + if (!(newSet.getValue()).equals(oldSet.getValue()) && !isExcluded(newSet.getName())) { + results.addChangedProperty(oldSet, newSet); + } + oldSettings.remove(oldSet); + } + newSettings.remove(newSet); + } + while (!oldSettings.isEmpty()) { + ODFSetting oldSet = oldSettings.get(0); + results.addRemovedProperty(oldSet); + } + return results; + } + + private static boolean isExcluded(String name) { + if (name.equals(RSID)) { + return true; + } + return false; + } + +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/SimpleSetting.java b/src/main/java/pro/litvinovg/docsettings/settings/SimpleSetting.java new file mode 100644 index 0000000..2fbcb0b --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/SimpleSetting.java @@ -0,0 +1,53 @@ +package pro.litvinovg.docsettings.settings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import pro.litvinovg.docsettings.exceptions.MalformedXmlException; + +public class SimpleSetting implements ODFSetting { + + private static final Logger logger = LoggerFactory.getLogger(SimpleSetting.class); + protected String name; + protected String value; + protected String type; + + public SimpleSetting(Node node) throws MalformedXmlException { + this.name = getNodeName(node); + this.value = node.getTextContent(); + this.type = node.getAttributes().getNamedItem("config:type").getNodeValue(); + } + protected String getNodeName(Node node) throws MalformedXmlException { + Node nameAttr = node.getAttributes().getNamedItem("config:name"); + if (nameAttr == null) { + logger.debug("name attribute not found while creating simple property"); + throw new MalformedXmlException("Node " + node.getNodeName() + " config:name attribute not found."); + } + return nameAttr.getNodeValue(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + @Override + public String getType() { + return type; + } + @Override + public void addToDocumentElement(Document doc, Node rootElement) { + Element element = doc.createElement("config:config-item"); + element.setAttribute("config:name", getName()); + element.setAttribute("config:type", getType()); + element.setTextContent(getValue()); + rootElement.appendChild(element); + } +} diff --git a/src/main/java/pro/litvinovg/docsettings/settings/StorageUtils.java b/src/main/java/pro/litvinovg/docsettings/settings/StorageUtils.java new file mode 100644 index 0000000..02f5ee2 --- /dev/null +++ b/src/main/java/pro/litvinovg/docsettings/settings/StorageUtils.java @@ -0,0 +1,126 @@ +package pro.litvinovg.docsettings.settings; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Base64; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import pro.litvinovg.docsettings.exceptions.MalformedXmlException; + +public class StorageUtils { + + private static final Logger logger = LoggerFactory.getLogger(StorageUtils.class); + + public static Document getDocFromString(String xmlString) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(new InputSource(new StringReader(xmlString))); + return doc; + } + + public static Document createNewDoc() throws ParserConfigurationException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.newDocument(); + return doc; + } + + public static String serializeDoc(Document doc) throws TransformerException { + StringWriter output = new StringWriter(); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.transform(new DOMSource(doc), new StreamResult(output)); + String str = output.toString(); + return str; + } + + public static String printNode(Node node) throws TransformerException { + StringWriter output = new StringWriter(); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.transform(new DOMSource(node), new StreamResult(output)); + String str = output.toString(); + return str; + } + + + public static String encodeBase64(String string) { + return new String(Base64.getEncoder().encode(string.getBytes())); + } + + public static String decodeBase64(String serializedMetaSettings) { + return new String(Base64.getDecoder().decode(serializedMetaSettings)); + } + + public static void removeNodes(NodeList nodes) { + for (int i = nodes.getLength()-1; i >= 0; i--) { + Node node = nodes.item(i); + node.getParentNode().removeChild(node); + } + } + + public static void appendSettingNodesToDoc(Settings settings, Document doc, Node root) { + for(int i = 0; i< settings.getSize();i++) { + ODFSetting setting = settings.get(i); + setting.addToDocumentElement(doc,root); + } + } + + public static Node getSettingsElement(Document doc) throws Exception { + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr = xpath.compile("//*[local-name() ='config-item-set'][@*[local-name() ='name' and .='ooo:configuration-settings']]"); + NodeList configItems = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + Node configSettings = configItems.item(0); + if (configSettings == null) { + throw new MalformedXmlException("No configuration settings element found"); + } + return configSettings; + } + + public static Node getMetaRootElement(Document doc) throws MalformedXmlException { + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression expr; + NodeList nodes; + try { + expr = xpath.compile("//*[local-name() ='meta']"); + nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + if (nodes.getLength() == 0 ) { + throw new MalformedXmlException("Xpath expression failed while looking for main meta element"); + } + } catch (XPathExpressionException e) { + logger.debug(e.getLocalizedMessage()); + logger.debug("Ops!",e); + throw new MalformedXmlException("Xpath expression failed while looking for main meta element"); + } + Node result = nodes.item(0); + return result; + } + + + +} diff --git a/src/test/java/pro/litvinovg/docsettings/exceptions/MalformedXmlExceptionTest.java b/src/test/java/pro/litvinovg/docsettings/exceptions/MalformedXmlExceptionTest.java new file mode 100644 index 0000000..b359e95 --- /dev/null +++ b/src/test/java/pro/litvinovg/docsettings/exceptions/MalformedXmlExceptionTest.java @@ -0,0 +1,14 @@ +package pro.litvinovg.docsettings.exceptions; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class MalformedXmlExceptionTest { + + @Test + void test() { + fail("Not yet implemented"); + } + +} diff --git a/src/test/java/pro/litvinovg/docsettings/processor/ODFSettingsTest.java b/src/test/java/pro/litvinovg/docsettings/processor/ODFSettingsTest.java new file mode 100644 index 0000000..0437935 --- /dev/null +++ b/src/test/java/pro/litvinovg/docsettings/processor/ODFSettingsTest.java @@ -0,0 +1,14 @@ +package pro.litvinovg.docsettings.processor; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ODFSettingsTest { + + @Test + void test() { + fail("Not yet implemented"); + } + +} diff --git a/testFiles/settings-old.xml b/testFiles/settings-old.xml new file mode 100644 index 0000000..dbd7bdd --- /dev/null +++ b/testFiles/settings-old.xml @@ -0,0 +1,131 @@ + + + + + 0 + 0 + 21625 + 9213 + true + false + + + view2 + 3401 + 2501 + 0 + 0 + 21624 + 9211 + 0 + 1 + false + 214 + false + true + + + + + false + + false + false + false + false + true + true + true + 0 + true + false + true + false + false + false + false + false + false + false + false + false + false + 517402 + true + false + true + true + true + true + false + false + true + false + false + false + true + false + false + false + false + false + 1 + 0 + + 0 + false + true + false + + false + false + + true + + false + true + false + false + + false + true + true + false + true + high-resolution + false + true + true + true + false + true + false + + true + false + false + false + false + false + false + false + true + false + true + 648908 + false + true + false + false + false + false + false + false + true + false + false + true + true + + + diff --git a/testFiles/settings.xml b/testFiles/settings.xml new file mode 100644 index 0000000..c457777 --- /dev/null +++ b/testFiles/settings.xml @@ -0,0 +1,131 @@ + + + + + 0 + 0 + 21625 + 9213 + true + false + + + view2 + 3401 + 2501 + 0 + 0 + 21624 + 9211 + 0 + 1 + false + 214 + false + true + + + + + false + + false + false + false + false + true + true + true + 0 + true + false + true + false + false + false + false + false + false + false + true + false + false + 517402 + false + false + true + false + true + true + false + false + true + false + true + true + false + false + false + true + false + false + 1 + 0 + + 0 + false + true + false + + false + false + + false + + false + true + false + false + + false + true + false + false + true + high-resolution + false + true + true + true + false + true + false + + true + false + false + false + false + false + false + false + true + false + true + 639374 + false + false + false + false + false + false + false + false + true + false + false + false + false + + + diff --git a/testFiles/testFile (copy).odt b/testFiles/testFile (copy).odt new file mode 100644 index 0000000..f77a76b Binary files /dev/null and b/testFiles/testFile (copy).odt differ diff --git a/testFiles/testFile.odt b/testFiles/testFile.odt new file mode 100644 index 0000000..97f16dd Binary files /dev/null and b/testFiles/testFile.odt differ