From 14fba9c09557f6fd7f6ae536204f2ab5ece6196a Mon Sep 17 00:00:00 2001 From: tiliavir Date: Sat, 5 Feb 2022 12:28:11 +0100 Subject: [PATCH 01/16] Dependency Update, JUnit5, Lombok, Code Style --- lombok.config | 2 + pom.xml | 81 +-- .../digitalsignature/ContextHelper.java | 88 +-- .../DigitalSignatureMacro.java | 605 +++++++++--------- .../digitalsignature/InheritSigners.java | 20 +- .../confluence/digitalsignature/Markdown.java | 24 +- .../digitalsignature/Signature.java | 313 ++++----- .../digitalsignature/SignaturesVisible.java | 18 +- .../digitalsignature/UserProfileByName.java | 27 +- .../api/DigitalSignatureComponent.java | 4 +- .../impl/DigitalSignatureComponentImpl.java | 30 +- .../rest/DigitalSigatureService.java | 286 ++++----- .../digitalsignature/sal/DummyProfile.java | 81 ++- .../META-INF/spring/plugin-context.xml | 4 +- src/main/resources/js/digital-signature.js | 64 +- src/main/resources/templates/export.vm | 32 +- src/main/resources/templates/macro.vm | 132 ++-- .../DigitalSignatureMacroTest.java | 26 +- .../digitalsignature/InheritSignersTest.java | 17 +- .../digitalsignature/MarkdownTest.java | 43 +- .../digitalsignature/MessageFormatTest.java | 18 +- .../SignatureSerialisationTest.java | 66 +- .../digitalsignature/SignatureTest.java | 19 +- .../digitalsignature/TemplatesTest.java | 66 +- .../UserProfileByNameTest.java | 38 +- .../digitalsignature/MyComponentUnitTest.java | 17 +- src/test/resources/atlassian-plugin.xml | 10 +- 27 files changed, 1012 insertions(+), 1119 deletions(-) create mode 100644 lombok.config diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..df71bb6 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true diff --git a/pom.xml b/pom.xml index 179ea6e..96d82d3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,23 +4,24 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + digital-signature + This is the com.baloise.confluence:digital-signature plugin for Atlassian Confluence. + atlassian-plugin 4.0.0 com.baloise.confluence digital-signature 7.0.5 + Baloise http://www.baloise.ch/ - digital-signature - This is the com.baloise.confluence:digital-signature plugin for Atlassian Confluence. - atlassian-plugin - Github https://github.com/baloise/digital-signature/issues + scm:git:https://github.com/baloise/digital-signature.git scm:git:https://github.com/baloise/digital-signature.git @@ -31,30 +32,21 @@ com.vladsch.flexmark flexmark - 0.60.2 + 0.62.2 - junit - junit - 4.13.1 - test + com.google.code.gson + gson + 2.8.9 - com.atlassian.confluence - confluence - ${confluence.version} - provided - - - - com.atlassian.mywork - mywork-api - 1.0.2 - provided + org.projectlombok + lombok + 1.18.22 + compile - com.atlassian.plugin atlassian-spring-scanner-annotation @@ -69,44 +61,57 @@ runtime + + com.atlassian.confluence + confluence + ${confluence.version} + provided + + + com.atlassian.mywork + mywork-api + 1.0.2 + provided + javax.inject javax.inject 1 provided - - org.slf4j - slf4j-simple - 2.0.0-alpha1 - test + javax.ws.rs + jsr311-api + 1.1.1 + provided + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + com.atlassian.plugins atlassian-plugins-osgi-testrunner ${plugin.testrunner.version} test - - javax.ws.rs - jsr311-api - 1.1.1 - provided - - - com.google.code.gson - gson - 2.2.2-atlassian-1 - org.mockito mockito-all - 1.9.0 + 1.10.19 + test + + + org.slf4j + slf4j-simple + 2.0.0-alpha1 test + diff --git a/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java b/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java index e610b22..ff65eaa 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java @@ -11,59 +11,59 @@ import static java.lang.String.format; public class ContextHelper { - public Object getOrderedSignatures(Signature signature) { - SortedSet> ret = new TreeSet<>(Comparator.comparing((Function, Date>) Entry::getValue) - .thenComparing(Entry::getKey)); - ret.addAll(signature.getSignatures().entrySet()); - return ret; - } + public Object getOrderedSignatures(Signature signature) { + SortedSet> ret = new TreeSet<>(Comparator.comparing((Function, Date>) Entry::getValue) + .thenComparing(Entry::getKey)); + ret.addAll(signature.getSignatures().entrySet()); + return ret; + } - @SafeVarargs - public final Map union(Map... maps) { - Map union = new HashMap<>(); - for (Map map : maps) { - union.putAll(map); - } - return union; + @SafeVarargs + public final Map union(Map... maps) { + Map union = new HashMap<>(); + for (Map map : maps) { + union.putAll(map); } + return union; + } - @SafeVarargs - public final Set union(Set... sets) { - Set union = new HashSet<>(); - for (Set set : sets) { - union.addAll(set); - } - return union; + @SafeVarargs + public final Set union(Set... sets) { + Set union = new HashSet<>(); + for (Set set : sets) { + union.addAll(set); } + return union; + } - public Map getProfiles(UserManager userManager, Set userNames) { - Map ret = new HashMap<>(); - if (Signature.isPetitionMode(userNames)) return ret; - for (String userName : userNames) { - ret.put(userName, getProfileNotNull(userManager, userName)); - } - return ret; + public Map getProfiles(UserManager userManager, Set userNames) { + Map ret = new HashMap<>(); + if (Signature.isPetitionMode(userNames)) return ret; + for (String userName : userNames) { + ret.put(userName, getProfileNotNull(userManager, userName)); } + return ret; + } - public UserProfile getProfileNotNull(UserManager userManager, String userName) { - UserProfile profile = userManager.getUserProfile(userName); - return profile == null ? new DummyProfile(userName) : profile; - } + public UserProfile getProfileNotNull(UserManager userManager, String userName) { + UserProfile profile = userManager.getUserProfile(userName); + return profile == null ? new DummyProfile(userName) : profile; + } - public SortedSet getOrderedProfiles(UserManager userManager, Set userNames) { - SortedSet ret = new TreeSet<>(new UserProfileByName()); - if (Signature.isPetitionMode(userNames)) return ret; - for (String userName : userNames) { - ret.add(getProfileNotNull(userManager, userName)); - } - return ret; + public SortedSet getOrderedProfiles(UserManager userManager, Set userNames) { + SortedSet ret = new TreeSet<>(new UserProfileByName()); + if (Signature.isPetitionMode(userNames)) return ret; + for (String userName : userNames) { + ret.add(getProfileNotNull(userManager, userName)); } + return ret; + } - public String mailTo(UserProfile profile) { - return format("%s<%s>", profile.getFullName().trim(), profile.getEmail().trim()); - } + public String mailTo(UserProfile profile) { + return format("%s<%s>", profile.getFullName().trim(), profile.getEmail().trim()); + } - public boolean hasEmail(UserProfile profile) { - return profile != null && profile.getEmail() != null && !profile.getEmail().trim().isEmpty(); - } + public boolean hasEmail(UserProfile profile) { + return profile != null && profile.getEmail() != null && !profile.getEmail().trim().isEmpty(); + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java index 00de8cc..c6d2cff 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java @@ -1,14 +1,5 @@ package com.baloise.confluence.digitalsignature; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.security.InvalidParameterException; -import java.util.*; - -import org.apache.velocity.tools.generic.DateTool; -import org.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Autowired; - import com.atlassian.bandana.BandanaManager; import com.atlassian.confluence.content.render.xhtml.ConversionContext; import com.atlassian.confluence.core.ContentEntityObject; @@ -32,6 +23,14 @@ import com.atlassian.user.Group; import com.atlassian.user.GroupManager; import com.atlassian.user.search.page.Pager; +import org.apache.velocity.tools.generic.DateTool; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.InvalidParameterException; +import java.util.*; import static com.atlassian.confluence.renderer.radeox.macros.MacroUtils.defaultVelocityContext; import static com.atlassian.confluence.security.ContentPermission.*; @@ -42,338 +41,310 @@ @Scanned public class DigitalSignatureMacro implements Macro { - private final int MAX_MAILTO_CHARACTER_COUNT = 500; - private final String REST_PATH = "/rest/signature/1.0"; - private final String DISPLAY_PATH = "/display"; - private final transient Markdown markdown = new Markdown(); - private final PermissionManager permissionManager; - private final Set all = new HashSet<>(); - private BandanaManager bandanaManager; - private UserManager userManager; - private BootstrapManager bootstrapManager; - private PageManager pageManager; - private ContextHelper contextHelper = new ContextHelper(); - private GroupManager groupManager; - private I18nResolver i18nResolver; - - @Autowired - public DigitalSignatureMacro( - @ComponentImport BandanaManager bandanaManager, - @ComponentImport UserManager userManager, - @ComponentImport BootstrapManager bootstrapManager, - @ComponentImport PageManager pageManager, - @ComponentImport PermissionManager permissionManager, - @ComponentImport GroupManager groupManager, - @ComponentImport I18nResolver i18nResolver - ) { - this.bandanaManager = bandanaManager; - this.userManager = userManager; - this.bootstrapManager = bootstrapManager; - this.pageManager = pageManager; - this.permissionManager = permissionManager; - this.groupManager = groupManager; - this.i18nResolver = i18nResolver; - all.add("*"); + private static final int MAX_MAILTO_CHARACTER_COUNT = 500; + private static final String REST_PATH = "/rest/signature/1.0"; + private static final String DISPLAY_PATH = "/display"; + + private final BandanaManager bandanaManager; + private final UserManager userManager; + private final BootstrapManager bootstrapManager; + private final PageManager pageManager; + private final PermissionManager permissionManager; + private final GroupManager groupManager; + private final I18nResolver i18nResolver; + private final transient Markdown markdown = new Markdown(); + private final Set all = new HashSet<>(); + private final ContextHelper contextHelper = new ContextHelper(); + + @Autowired + public DigitalSignatureMacro(@ComponentImport BandanaManager bandanaManager, @ComponentImport UserManager userManager, @ComponentImport BootstrapManager bootstrapManager, @ComponentImport PageManager pageManager, @ComponentImport PermissionManager permissionManager, @ComponentImport GroupManager groupManager, @ComponentImport I18nResolver i18nResolver) { + this.bandanaManager = bandanaManager; + this.userManager = userManager; + this.bootstrapManager = bootstrapManager; + this.pageManager = pageManager; + this.permissionManager = permissionManager; + this.groupManager = groupManager; + this.i18nResolver = i18nResolver; + all.add("*"); + } + + @Override + public String execute(Map params, String body, ConversionContext conversionContext) { + if (body == null || body.length() <= 10) { + return warning(i18nResolver.getText("com.baloise.confluence.digital-signature.signature.macro.warning.bodyToShort")); } - @Override - public String execute(Map params, String body, ConversionContext conversionContext) { - if (body == null || body.length() <= 10) { - return warning(i18nResolver.getText("com.baloise.confluence.digital-signature.signature.macro.warning.bodyToShort")); - } - - Set userGroups = getSet(params, "signerGroups"); - boolean petitionMode = Signature.isPetitionMode(userGroups); - @SuppressWarnings("unchecked") - Set signers = petitionMode ? all : contextHelper.union( - getSet(params, "signers"), - loadUserGroups(userGroups), - loadInheritedSigners(InheritSigners.ofValue(params.get("inheritSigners")), conversionContext) - ); - ContentEntityObject entity = conversionContext.getEntity(); - Signature signature = sync(new Signature( - entity.getLatestVersionId(), - body, - params.get("title")) - .withNotified(getSet(params, "notified")) - .withMaxSignatures(getLong(params, "maxSignatures", -1)) - .withVisibilityLimit(getLong(params, "visibilityLimit", -1)), - signers - ); - - boolean protectedContent = getBoolean(params, "protectedContent", false); - if (protectedContent && isPage(conversionContext)) { - try { - ensureProtectedPage(conversionContext, (Page) entity, signature); - } catch (Exception e) { - return warning(i18nResolver.getText("com.baloise.confluence.digital-signature.signature.macro.warning.editPermissionRequiredForProtectedContent", "")); - } - } - - return getRenderedTemplate("templates/macro.vm", buildContext(params, conversionContext, entity, signature, protectedContent)); + Set userGroups = getSet(params, "signerGroups"); + boolean petitionMode = Signature.isPetitionMode(userGroups); + Set signers = petitionMode ? all : contextHelper.union(getSet(params, "signers"), loadUserGroups(userGroups), loadInheritedSigners(InheritSigners.ofValue(params.get("inheritSigners")), conversionContext)); + ContentEntityObject entity = conversionContext.getEntity(); + Signature signature = sync(new Signature(entity.getLatestVersionId(), body, params.get("title")).withNotified(getSet(params, "notified")).withMaxSignatures(getLong(params, "maxSignatures", -1)).withVisibilityLimit(getLong(params, "visibilityLimit", -1)), signers); + + boolean protectedContent = getBoolean(params, "protectedContent", false); + if (protectedContent && isPage(conversionContext)) { + try { + ensureProtectedPage(conversionContext, (Page) entity, signature); + } catch (Exception e) { + return warning(i18nResolver.getText("com.baloise.confluence.digital-signature.signature.macro.warning.editPermissionRequiredForProtectedContent", "")); + } } - @NotNull - private Map buildContext(Map params, ConversionContext conversionContext, ContentEntityObject page, Signature signature, boolean protectedContent) { - ConfluenceUser currentUser = AuthenticatedUserThreadLocal.get(); - String currentUserName = currentUser.getName(); - boolean protectedContentAccess = protectedContent && (permissionManager.hasPermission(currentUser, Permission.EDIT, page) || signature.hasSigned(currentUserName)); + return getRenderedTemplate("templates/macro.vm", buildContext(params, conversionContext, entity, signature, protectedContent)); + } - Map context = defaultVelocityContext(); - context.put("date", new DateTool()); - context.put("markdown", markdown); + @NotNull + private Map buildContext(Map params, ConversionContext conversionContext, ContentEntityObject page, Signature signature, boolean protectedContent) { + ConfluenceUser currentUser = AuthenticatedUserThreadLocal.get(); + String currentUserName = currentUser.getName(); + boolean protectedContentAccess = protectedContent && (permissionManager.hasPermission(currentUser, Permission.EDIT, page) || signature.hasSigned(currentUserName)); - if (signature.isSignatureMissing(currentUserName)) { - context.put("signAs", contextHelper.getProfileNotNull(userManager, currentUserName).getFullName()); - context.put("signAction", bootstrapManager.getWebAppContextPath() + REST_PATH + "/sign"); - } - context.put("panel", getBoolean(params, "panel", true)); - context.put("protectedContent", protectedContentAccess); - if (protectedContentAccess && isPage(conversionContext)) { - context.put("protectedContentURL", bootstrapManager.getWebAppContextPath() + DISPLAY_PATH + "/" + ((Page) page).getSpaceKey() + "/" + signature.getProtectedKey()); - } + Map context = defaultVelocityContext(); + context.put("date", new DateTool()); + context.put("markdown", markdown); - boolean canExport = hideSignatures(params, signature, currentUserName); - Map signed = contextHelper.getProfiles(userManager, signature.getSignatures().keySet()); - Map missing = contextHelper.getProfiles(userManager, signature.getMissingSignatures()); - - context.put("orderedSignatures", contextHelper.getOrderedSignatures(signature)); - context.put("orderedMissingSignatureProfiles", contextHelper.getOrderedProfiles(userManager, signature.getMissingSignatures())); - context.put("profiles", contextHelper.union(signed, missing)); - context.put("signature", signature); - context.put("visibilityLimit", signature.getVisibilityLimit()); - context.put("mailtoSigned", getMailto(signed.values(), signature.getTitle(), true, signature)); - context.put("mailtoMissing", getMailto(missing.values(), signature.getTitle(), false, signature)); - context.put("UUID", UUID.randomUUID().toString().replace("-", "")); - context.put("downloadURL", canExport ? bootstrapManager.getWebAppContextPath() + REST_PATH + "/export?key=" + signature.getKey() : null); - return context; + if (signature.isSignatureMissing(currentUserName)) { + context.put("signAs", contextHelper.getProfileNotNull(userManager, currentUserName).getFullName()); + context.put("signAction", bootstrapManager.getWebAppContextPath() + REST_PATH + "/sign"); } - - private void ensureProtectedPage(ConversionContext conversionContext, Page page, Signature signature) { - Page protectedPage = pageManager.getPage(conversionContext.getSpaceKey(), signature.getProtectedKey()); - if (protectedPage == null) { - ContentPermissionSet editors = page.getContentPermissionSet(EDIT_PERMISSION); - if (editors == null || editors.size() == 0) { - throw new IllegalStateException("No editors found!"); - } - protectedPage = new Page(); - protectedPage.setSpace(page.getSpace()); - protectedPage.setParentPage(page); - protectedPage.setVersion(1); - protectedPage.setCreator(page.getCreator()); - for (ContentPermission editor : editors) { - protectedPage.addPermission(createUserPermission(EDIT_PERMISSION, editor.getUserSubject())); - protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, editor.getUserSubject())); - } - for (String signedUserName : signature.getSignatures().keySet()) { - protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, signedUserName)); - } - protectedPage.setTitle(signature.getProtectedKey()); - pageManager.saveContentEntity(protectedPage, DefaultSaveContext.DEFAULT); - page.addChild(protectedPage); - } + context.put("panel", getBoolean(params, "panel", true)); + context.put("protectedContent", protectedContentAccess); + if (protectedContentAccess && isPage(conversionContext)) { + context.put("protectedContentURL", bootstrapManager.getWebAppContextPath() + DISPLAY_PATH + "/" + ((Page) page).getSpaceKey() + "/" + signature.getProtectedKey()); } - private boolean hideSignatures(Map params, Signature signature, String currentUserName) { - try { - signature = signature.clone(); - } catch (CloneNotSupportedException e) { - throw new IllegalStateException(e); - } - boolean pendingVisible = isVisible(signature, currentUserName, params.get("pendingVisible")); - boolean signaturesVisible = isVisible(signature, currentUserName, params.get("signaturesVisible")); - if (!pendingVisible) signature.setMissingSignatures(new TreeSet<>()); - if (!signaturesVisible) signature.setSignatures(new HashMap<>()); - return pendingVisible && signaturesVisible; + boolean canExport = hideSignatures(params, signature, currentUserName); + Map signed = contextHelper.getProfiles(userManager, signature.getSignatures().keySet()); + Map missing = contextHelper.getProfiles(userManager, signature.getMissingSignatures()); + + context.put("orderedSignatures", contextHelper.getOrderedSignatures(signature)); + context.put("orderedMissingSignatureProfiles", contextHelper.getOrderedProfiles(userManager, signature.getMissingSignatures())); + context.put("profiles", contextHelper.union(signed, missing)); + context.put("signature", signature); + context.put("visibilityLimit", signature.getVisibilityLimit()); + context.put("mailtoSigned", getMailto(signed.values(), signature.getTitle(), true, signature)); + context.put("mailtoMissing", getMailto(missing.values(), signature.getTitle(), false, signature)); + context.put("UUID", UUID.randomUUID().toString().replace("-", "")); + context.put("downloadURL", canExport ? bootstrapManager.getWebAppContextPath() + REST_PATH + "/export?key=" + signature.getKey() : null); + return context; + } + + private void ensureProtectedPage(ConversionContext conversionContext, Page page, Signature signature) { + Page protectedPage = pageManager.getPage(conversionContext.getSpaceKey(), signature.getProtectedKey()); + if (protectedPage == null) { + ContentPermissionSet editors = page.getContentPermissionSet(EDIT_PERMISSION); + if (editors == null || editors.size() == 0) { + throw new IllegalStateException("No editors found!"); + } + protectedPage = new Page(); + protectedPage.setSpace(page.getSpace()); + protectedPage.setParentPage(page); + protectedPage.setVersion(1); + protectedPage.setCreator(page.getCreator()); + for (ContentPermission editor : editors) { + protectedPage.addPermission(createUserPermission(EDIT_PERMISSION, editor.getUserSubject())); + protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, editor.getUserSubject())); + } + for (String signedUserName : signature.getSignatures().keySet()) { + protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, signedUserName)); + } + protectedPage.setTitle(signature.getProtectedKey()); + pageManager.saveContentEntity(protectedPage, DefaultSaveContext.DEFAULT); + page.addChild(protectedPage); } + } - private boolean isVisible(Signature signature, String currentUserName, String signaturesVisibleParam) { - switch (SignaturesVisible.ofValue(signaturesVisibleParam)) { - case IF_SIGNATORY: - return signature.hasSigned(currentUserName) || signature.isSignatory(currentUserName); - case IF_SIGNED: - return signature.hasSigned(currentUserName); - case ALWAYS: - return true; - default: - throw new InvalidParameterException(String.format("'%s' is an unknown value of SignaturesVisible!", signaturesVisibleParam)); - } + private boolean hideSignatures(Map params, Signature signature, String currentUserName) { + try { + signature = signature.clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException(e); } - - private boolean isPage(ConversionContext conversionContext) { - return conversionContext.getEntity() instanceof Page; + boolean pendingVisible = isVisible(signature, currentUserName, params.get("pendingVisible")); + boolean signaturesVisible = isVisible(signature, currentUserName, params.get("signaturesVisible")); + if (!pendingVisible) signature.setMissingSignatures(new TreeSet<>()); + if (!signaturesVisible) signature.setSignatures(new HashMap<>()); + return pendingVisible && signaturesVisible; + } + + private boolean isVisible(Signature signature, String currentUserName, String signaturesVisibleParam) { + switch (SignaturesVisible.ofValue(signaturesVisibleParam)) { + case IF_SIGNATORY: + return signature.hasSigned(currentUserName) || signature.isSignatory(currentUserName); + case IF_SIGNED: + return signature.hasSigned(currentUserName); + case ALWAYS: + return true; + default: + throw new InvalidParameterException(String.format("'%s' is an unknown value of SignaturesVisible!", signaturesVisibleParam)); } - - private String warning(String message) { - return "
\n" + - "

\n" + - " " + i18nResolver.getText("com.baloise.confluence.digital-signature.signature.label") + "\n" + - "

\n" + - "

" + message + "

\n" + - "
"; + } + + private boolean isPage(ConversionContext conversionContext) { + return conversionContext.getEntity() instanceof Page; + } + + private String warning(String message) { + return "
\n" + "

\n" + " " + i18nResolver.getText("com.baloise.confluence.digital-signature.signature.label") + "\n" + "

\n" + "

" + message + "

\n" + "
"; + } + + private Set loadInheritedSigners(InheritSigners inheritSigners, ConversionContext conversionContext) { + Set users = new HashSet<>(); + switch (inheritSigners) { + case READERS_AND_WRITERS: + users.addAll(loadUsers(conversionContext, VIEW_PERMISSION)); + users.addAll(loadUsers(conversionContext, EDIT_PERMISSION)); + break; + case READERS_ONLY: + users.addAll(loadUsers(conversionContext, VIEW_PERMISSION)); + users.removeAll(loadUsers(conversionContext, EDIT_PERMISSION)); + break; + case WRITERS_ONLY: + users.addAll(loadUsers(conversionContext, EDIT_PERMISSION)); + break; + case NONE: + break; + default: + throw new IllegalArgumentException(inheritSigners + " is unknown or not yet implemented!"); } - - private Set loadInheritedSigners(InheritSigners inheritSigners, ConversionContext conversionContext) { - Set users = new HashSet<>(); - switch (inheritSigners) { - case READERS_AND_WRITERS: - users.addAll(loadUsers(conversionContext, VIEW_PERMISSION)); - users.addAll(loadUsers(conversionContext, EDIT_PERMISSION)); - break; - case READERS_ONLY: - users.addAll(loadUsers(conversionContext, VIEW_PERMISSION)); - users.removeAll(loadUsers(conversionContext, EDIT_PERMISSION)); - break; - case WRITERS_ONLY: - users.addAll(loadUsers(conversionContext, EDIT_PERMISSION)); - break; - case NONE: - break; - default: - throw new IllegalArgumentException(inheritSigners + " is unknown or not yet implemented!"); + return users; + } + + private Set loadUsers(ConversionContext conversionContext, String permission) { + Set users = new HashSet<>(); + ContentPermissionSet contentPermissionSet = conversionContext.getEntity().getContentPermissionSet(permission); + if (contentPermissionSet != null) { + for (ContentPermission cp : contentPermissionSet) { + if (cp.getGroupName() != null) { + users.addAll(loadUserGroup(cp.getGroupName())); } - return users; - } - - private Set loadUsers(ConversionContext conversionContext, String permission) { - Set users = new HashSet<>(); - ContentPermissionSet contentPermissionSet = conversionContext.getEntity().getContentPermissionSet(permission); - if (contentPermissionSet != null) { - for (ContentPermission cp : contentPermissionSet) { - if (cp.getGroupName() != null) { - users.addAll(loadUserGroup(cp.getGroupName())); - } - if (cp.getUserSubject() != null) { - users.add(cp.getUserSubject().getName()); - } - } + if (cp.getUserSubject() != null) { + users.add(cp.getUserSubject().getName()); } - return users; - } - - private Set loadUserGroups(Iterable groupNames) { - Set ret = new HashSet<>(); - for (String groupName : groupNames) { - ret.addAll(loadUserGroup(groupName)); - } - return ret; - } - - private Set loadUserGroup(String groupName) { - Set ret = new HashSet<>(); - try { - if (groupName == null) return ret; - Group group = groupManager.getGroup(groupName.trim()); - if (group == null) return ret; - Pager pager = groupManager.getMemberNames(group); - while (!pager.onLastPage()) { - ret.addAll(pager.getCurrentPage()); - pager.nextPage(); - } - ret.addAll(pager.getCurrentPage()); - } catch (EntityException e) { - e.printStackTrace(); - } - return ret; - } - - private Boolean getBoolean(Map params, String key, Boolean fallback) { - String value = params.get(key); - return value == null ? fallback : Boolean.valueOf(value); + } } + return users; + } - private long getLong(Map params, String key, long fallback) { - String value = params.get(key); - return value == null ? fallback : Long.parseLong(value); + private Set loadUserGroups(Iterable groupNames) { + Set ret = new HashSet<>(); + for (String groupName : groupNames) { + ret.addAll(loadUserGroup(groupName)); } - - private Set getSet(Map params, String key) { - String value = params.get(key); - return value == null || value.trim().isEmpty() ? new TreeSet<>() : new TreeSet<>(asList(value.split("[;, ]+"))); + return ret; + } + + private Set loadUserGroup(String groupName) { + Set ret = new HashSet<>(); + try { + if (groupName == null) return ret; + Group group = groupManager.getGroup(groupName.trim()); + if (group == null) return ret; + Pager pager = groupManager.getMemberNames(group); + while (!pager.onLastPage()) { + ret.addAll(pager.getCurrentPage()); + pager.nextPage(); + } + ret.addAll(pager.getCurrentPage()); + } catch (EntityException e) { + e.printStackTrace(); } - - private Signature sync(Signature signature, Set signers) { - Signature loaded = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, signature.getKey()); - if (loaded != null) { - signature.setSignatures(loaded.getSignatures()); - boolean save = false; - - if (!Objects.equals(loaded.getNotify(), signature.getNotify())) { - loaded.setNotify(signature.getNotify()); - save = true; - } - - signers.removeAll(loaded.getSignatures().keySet()); - signature.setMissingSignatures(signers); - if (!Objects.equals(loaded.getMissingSignatures(), signature.getMissingSignatures())) { - loaded.setMissingSignatures(signature.getMissingSignatures()); - save = true; - } - - if (loaded.getMaxSignatures() != signature.getMaxSignatures()) { - loaded.setMaxSignatures(signature.getMaxSignatures()); - save = true; - } - - if (loaded.getVisibilityLimit() != signature.getVisibilityLimit()) { - loaded.setVisibilityLimit(signature.getVisibilityLimit()); - save = true; - } - - if (save) save(loaded); - } else { - signature.setMissingSignatures(signers); - save(signature); - } - return signature; + return ret; + } + + private Boolean getBoolean(Map params, String key, Boolean fallback) { + String value = params.get(key); + return value == null ? fallback : Boolean.valueOf(value); + } + + private long getLong(Map params, String key, long fallback) { + String value = params.get(key); + return value == null ? fallback : Long.parseLong(value); + } + + private Set getSet(Map params, String key) { + String value = params.get(key); + return value == null || value.trim().isEmpty() ? new TreeSet<>() : new TreeSet<>(asList(value.split("[;, ]+"))); + } + + private Signature sync(Signature signature, Set signers) { + Signature loaded = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, signature.getKey()); + if (loaded != null) { + signature.setSignatures(loaded.getSignatures()); + boolean save = false; + + if (!Objects.equals(loaded.getNotify(), signature.getNotify())) { + loaded.setNotify(signature.getNotify()); + save = true; + } + + signers.removeAll(loaded.getSignatures().keySet()); + signature.setMissingSignatures(signers); + if (!Objects.equals(loaded.getMissingSignatures(), signature.getMissingSignatures())) { + loaded.setMissingSignatures(signature.getMissingSignatures()); + save = true; + } + + if (loaded.getMaxSignatures() != signature.getMaxSignatures()) { + loaded.setMaxSignatures(signature.getMaxSignatures()); + save = true; + } + + if (loaded.getVisibilityLimit() != signature.getVisibilityLimit()) { + loaded.setVisibilityLimit(signature.getVisibilityLimit()); + save = true; + } + + if (save) save(loaded); + } else { + signature.setMissingSignatures(signers); + save(signature); } - - private void save(Signature signature) { - if (signature.hasMissingSignatures()) - bandanaManager.setValue(GLOBAL_CONTEXT, signature.getKey(), signature); + return signature; + } + + private void save(Signature signature) { + if (signature.hasMissingSignatures()) bandanaManager.setValue(GLOBAL_CONTEXT, signature.getKey(), signature); + } + + @Override + public BodyType getBodyType() { + return BodyType.PLAIN_TEXT; + } + + @Override + public OutputType getOutputType() { + return OutputType.BLOCK; + } + + protected String getMailto(Collection profiles, String subject, boolean signed, Signature signature) { + if (profiles == null || profiles.isEmpty()) return null; + Collection profilesWithMail = profiles.stream().filter(contextHelper::hasEmail).collect(toList()); + StringBuilder ret = new StringBuilder("mailto:"); + for (UserProfile profile : profilesWithMail) { + if (ret.length() > 7) ret.append(','); + ret.append(contextHelper.mailTo(profile)); } - - @Override - public BodyType getBodyType() { - return BodyType.PLAIN_TEXT; + ret.append("?Subject=").append(urlEncode(subject)); + if (ret.length() > MAX_MAILTO_CHARACTER_COUNT) { + ret.setLength(0); + ret.append("mailto:"); + for (UserProfile profile : profilesWithMail) { + if (ret.length() > 7) ret.append(','); + ret.append(profile.getEmail().trim()); + } + ret.append("?Subject=").append(urlEncode(subject)); } - - @Override - public OutputType getOutputType() { - return OutputType.BLOCK; + if (ret.length() > MAX_MAILTO_CHARACTER_COUNT) { + return bootstrapManager.getWebAppContextPath() + REST_PATH + "/emails?key=" + signature.getKey() + "&signed=" + signed; } - - protected String getMailto(Collection profiles, String subject, boolean signed, Signature signature) { - if (profiles == null || profiles.isEmpty()) return null; - Collection profilesWithMail = profiles.stream() - .filter(contextHelper::hasEmail) - .collect(toList()); - StringBuilder ret = new StringBuilder("mailto:"); - for (UserProfile profile : profilesWithMail) { - if (ret.length() > 7) ret.append(','); - ret.append(contextHelper.mailTo(profile)); - } - ret.append("?Subject=").append(urlEncode(subject)); - if (ret.length() > MAX_MAILTO_CHARACTER_COUNT) { - ret.setLength(0); - ret.append("mailto:"); - for (UserProfile profile : profilesWithMail) { - if (ret.length() > 7) ret.append(','); - ret.append(profile.getEmail().trim()); - } - ret.append("?Subject=").append(urlEncode(subject)); - } - if (ret.length() > MAX_MAILTO_CHARACTER_COUNT) { - return bootstrapManager.getWebAppContextPath() + REST_PATH + "/emails?key=" + signature.getKey() + "&signed=" + signed; - } - return ret.toString(); - } - - public String urlEncode(String string) { - try { - return URLEncoder.encode(string, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } + return ret.toString(); + } + + public String urlEncode(String string) { + try { + return URLEncoder.encode(string, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); } + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/InheritSigners.java b/src/main/java/com/baloise/confluence/digitalsignature/InheritSigners.java index c603032..7694e53 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/InheritSigners.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/InheritSigners.java @@ -1,16 +1,16 @@ package com.baloise.confluence.digitalsignature; public enum InheritSigners { - NONE, - READERS_AND_WRITERS, - READERS_ONLY, - WRITERS_ONLY; + NONE, + READERS_AND_WRITERS, + READERS_ONLY, + WRITERS_ONLY; - public static InheritSigners ofValue(String v) { - try { - return InheritSigners.valueOf(v.toUpperCase().replaceAll("\\W+", "_")); - } catch (Exception e) { - return NONE; - } + public static InheritSigners ofValue(String v) { + try { + return InheritSigners.valueOf(v.toUpperCase().replaceAll("\\W+", "_")); + } catch (Exception e) { + return NONE; } + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Markdown.java b/src/main/java/com/baloise/confluence/digitalsignature/Markdown.java index abf2f99..ee3199b 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Markdown.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Markdown.java @@ -5,19 +5,19 @@ import com.vladsch.flexmark.util.data.MutableDataSet; public class Markdown { - private final Parser parser; - private final HtmlRenderer renderer; + private final Parser parser; + private final HtmlRenderer renderer; - public Markdown() { - MutableDataSet options = new MutableDataSet(); - options.set(HtmlRenderer.DO_NOT_RENDER_LINKS, true); - options.set(HtmlRenderer.ESCAPE_HTML, true); + public Markdown() { + MutableDataSet options = new MutableDataSet(); + options.set(HtmlRenderer.DO_NOT_RENDER_LINKS, true); + options.set(HtmlRenderer.ESCAPE_HTML, true); - parser = Parser.builder(options).build(); - renderer = HtmlRenderer.builder(options).build(); - } + parser = Parser.builder(options).build(); + renderer = HtmlRenderer.builder(options).build(); + } - public String toHTML(String markdown) { - return renderer.render(parser.parse(markdown)); - } + public String toHTML(String markdown) { + return renderer.render(parser.parse(markdown)); + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index 57e23ae..b987ed1 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -1,203 +1,126 @@ package com.baloise.confluence.digitalsignature; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; import java.io.Serializable; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; +@Getter +@Setter +@NoArgsConstructor public class Signature implements Serializable, Cloneable { - - private static final long serialVersionUID = 1L; - - private String key = ""; - private String hash = ""; - private long pageId; - private String title = ""; - private String body = ""; - private long maxSignatures = -1; - private long visibilityLimit = -1; - private Map signatures = new HashMap<>(); - private Set missingSignatures = new TreeSet<>(); - private Set notified = new TreeSet<>(); - - public Signature() { - } - - public Signature(long pageId, String body, String title) { - this.pageId = pageId; - this.body = body; - this.title = title == null ? "" : title; - hash = sha256Hex(pageId + ":" + title + ":" + body); - key = "signature." + hash; - } - - public static boolean isPetitionMode(Set userGroups) { - return userGroups != null && userGroups.size() == 1 && userGroups.iterator().next().trim().equals("*"); - } - - public String getHash() { - if (hash == null) { - hash = getKey().replace("signature.", ""); - } - return hash; - } - - public void setHash(String hash) { - this.hash = hash; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String getProtectedKey() { - return "protected." + getHash(); - } - - public long getPageId() { - return pageId; - } - - public void setPageId(long pageId) { - this.pageId = pageId; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - - public Map getSignatures() { - return signatures; - } - - public void setSignatures(Map signatures) { - this.signatures = signatures; - } - - public Set getMissingSignatures() { - return missingSignatures; - } - - public void setMissingSignatures(Set missingSignatures) { - this.missingSignatures = missingSignatures; - } - - public long getVisibilityLimit() { - return visibilityLimit; - } - - public void setVisibilityLimit(long visibilityLimit) { - this.visibilityLimit = visibilityLimit; - } - - public long getMaxSignatures() { - return maxSignatures; - } - - public void setMaxSignatures(long maxSignatures) { - this.maxSignatures = maxSignatures; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Set getNotify() { - return notified; - } - - public void setNotify(Set notify) { - this.notified = notify; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((key == null) ? 0 : key.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Signature other = (Signature) obj; - if (key == null) { - return other.key == null; - } else return key.equals(other.key); - } - - public Signature withNotified(Set notified) { - this.notified = notified; - return this; - } - - public Signature withMaxSignatures(long maxSignatures) { - this.maxSignatures = maxSignatures; - return this; - } - - public Signature withVisibilityLimit(long visibilityLimit) { - this.visibilityLimit = visibilityLimit; - return this; - } - - public boolean hasSigned(String userName) { - return signatures.containsKey(userName); - } - - public boolean isPetitionMode() { - return isPetitionMode(getMissingSignatures()); - } - - public boolean sign(String userName) { - if (!isMaxSignaturesReached() && !isPetitionMode() && !getMissingSignatures().remove(userName)) { - return false; - } else { - getSignatures().put(userName, new Date()); - return true; - } - } - - public boolean isMaxSignaturesReached() { - return maxSignatures > -1 && maxSignatures <= getSignatures().size(); - } - - public boolean isSignatureMissing(String userName) { - return !isMaxSignaturesReached() && !hasSigned(userName) && isSignatory(userName); - } - - public boolean isSignatory(String userName) { - return isPetitionMode() || getMissingSignatures().contains(userName); - } - - public boolean hasMissingSignatures() { - return !isMaxSignaturesReached() && (isPetitionMode() || !getMissingSignatures().isEmpty()); - } - - @Override - public Signature clone() throws CloneNotSupportedException{ - return (Signature) super.clone(); - } + private static final long serialVersionUID = 1L; + + private String key = ""; + private String hash = ""; + private long pageId; + private String title = ""; + private String body = ""; + private long maxSignatures = -1; + private long visibilityLimit = -1; + private Map signatures = new HashMap<>(); + private Set missingSignatures = new TreeSet<>(); + private Set notify = new TreeSet<>(); + + public Signature(long pageId, String body, String title) { + this.pageId = pageId; + this.body = body; + this.title = title == null ? "" : title; + hash = sha256Hex(pageId + ":" + title + ":" + body); + key = "signature." + hash; + } + + public static boolean isPetitionMode(Set userGroups) { + return userGroups != null + && userGroups.size() == 1 + && userGroups.iterator().next().trim().equals("*"); + } + + public String getHash() { + if (hash == null) { + hash = getKey().replace("signature.", ""); + } + return hash; + } + + public String getProtectedKey() { + return "protected." + getHash(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + return Objects.equals(key, ((Signature) obj).key); + } + + public Signature withNotified(Set notified) { + this.notify = notified; + return this; + } + + public Signature withMaxSignatures(long maxSignatures) { + this.maxSignatures = maxSignatures; + return this; + } + + public Signature withVisibilityLimit(long visibilityLimit) { + this.visibilityLimit = visibilityLimit; + return this; + } + + public boolean hasSigned(String userName) { + return signatures.containsKey(userName); + } + + public boolean isPetitionMode() { + return isPetitionMode(getMissingSignatures()); + } + + public boolean sign(String userName) { + if (!isMaxSignaturesReached() && !isPetitionMode() && !getMissingSignatures().remove(userName)) { + return false; + } + + getSignatures().put(userName, new Date()); + return true; + } + + public boolean isMaxSignaturesReached() { + return maxSignatures > -1 && maxSignatures <= getSignatures().size(); + } + + public boolean isSignatureMissing(String userName) { + return !isMaxSignaturesReached() && !hasSigned(userName) && isSignatory(userName); + } + + public boolean isSignatory(String userName) { + return isPetitionMode() || getMissingSignatures().contains(userName); + } + + public boolean hasMissingSignatures() { + return !isMaxSignaturesReached() && (isPetitionMode() || !getMissingSignatures().isEmpty()); + } + + @Override + public Signature clone() throws CloneNotSupportedException { + return (Signature) super.clone(); + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/SignaturesVisible.java b/src/main/java/com/baloise/confluence/digitalsignature/SignaturesVisible.java index 417b28b..103d213 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/SignaturesVisible.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/SignaturesVisible.java @@ -1,15 +1,15 @@ package com.baloise.confluence.digitalsignature; public enum SignaturesVisible { - ALWAYS, - IF_SIGNATORY, - IF_SIGNED; + ALWAYS, + IF_SIGNATORY, + IF_SIGNED; - public static SignaturesVisible ofValue(String v) { - try { - return SignaturesVisible.valueOf(v.toUpperCase().replaceAll("\\W+", "_")); - } catch (Exception e) { - return ALWAYS; - } + public static SignaturesVisible ofValue(String v) { + try { + return SignaturesVisible.valueOf(v.toUpperCase().replaceAll("\\W+", "_")); + } catch (Exception e) { + return ALWAYS; } + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/UserProfileByName.java b/src/main/java/com/baloise/confluence/digitalsignature/UserProfileByName.java index ea02d57..1687b06 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/UserProfileByName.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/UserProfileByName.java @@ -5,22 +5,21 @@ import java.util.Comparator; public class UserProfileByName implements Comparator { + @Override + public int compare(UserProfile u1, UserProfile u2) { + int ret = nn(u1.getFullName()).compareTo(nn(u2.getFullName())); + if (ret != 0) return ret; - @Override - public int compare(UserProfile u1, UserProfile u2) { - int ret = nn(u1.getFullName()).compareTo(nn(u2.getFullName())); - if (ret != 0) return ret; + ret = nn(u1.getEmail()).compareTo(nn(u2.getEmail())); + if (ret != 0) return ret; - ret = nn(u1.getEmail()).compareTo(nn(u2.getEmail())); - if (ret != 0) return ret; + ret = nn(u1.getUsername()).compareTo(nn(u2.getUsername())); + if (ret != 0) return ret; - ret = nn(u1.getUsername()).compareTo(nn(u2.getUsername())); - if (ret != 0) return ret; + return Integer.compare(u1.hashCode(), u2.hashCode()); + } - return Integer.compare(u1.hashCode(), u2.hashCode()); - } - - private String nn(String string) { - return string == null ? "" : string; - } + private String nn(String string) { + return string == null ? "" : string; + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/api/DigitalSignatureComponent.java b/src/main/java/com/baloise/confluence/digitalsignature/api/DigitalSignatureComponent.java index 3825a8e..41dda06 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/api/DigitalSignatureComponent.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/api/DigitalSignatureComponent.java @@ -1,7 +1,7 @@ package com.baloise.confluence.digitalsignature.api; public interface DigitalSignatureComponent { - String PLUGIN_KEY = "com.baloise.confluence:digital-signature"; + String PLUGIN_KEY = "com.baloise.confluence:digital-signature"; - String getName(); + String getName(); } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/impl/DigitalSignatureComponentImpl.java b/src/main/java/com/baloise/confluence/digitalsignature/impl/DigitalSignatureComponentImpl.java index 379b812..4a49a44 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/impl/DigitalSignatureComponentImpl.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/impl/DigitalSignatureComponentImpl.java @@ -11,23 +11,23 @@ @ExportAsService({DigitalSignatureComponent.class}) @Named("digitalSignatureComponent") public class DigitalSignatureComponentImpl implements DigitalSignatureComponent { - @ComponentImport - private final ApplicationProperties applicationProperties; + @ComponentImport + private final ApplicationProperties applicationProperties; - public DigitalSignatureComponentImpl() { - this(null); - } - - @Inject - public DigitalSignatureComponentImpl(final ApplicationProperties applicationProperties) { - this.applicationProperties = applicationProperties; - } + public DigitalSignatureComponentImpl() { + this(null); + } - public String getName() { - if (null != applicationProperties) { - return "digitalSignatureComponent:" + applicationProperties.getDisplayName(); - } + @Inject + public DigitalSignatureComponentImpl(final ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } - return "digitalSignatureComponent"; + public String getName() { + if (null != applicationProperties) { + return "digitalSignatureComponent:" + applicationProperties.getDisplayName(); } + + return "digitalSignatureComponent"; + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java index 433bca9..58e447c 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java @@ -6,7 +6,6 @@ import com.atlassian.confluence.setup.settings.SettingsManager; import com.atlassian.confluence.user.AuthenticatedUserThreadLocal; import com.atlassian.confluence.user.ConfluenceUser; -import com.atlassian.confluence.velocity.htmlsafe.HtmlSafe; import com.atlassian.mail.Email; import com.atlassian.mail.MailException; import com.atlassian.mail.server.MailServerManager; @@ -14,22 +13,19 @@ import com.atlassian.mywork.model.NotificationBuilder; import com.atlassian.mywork.service.LocalNotificationService; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; -import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import com.atlassian.sal.api.message.I18nResolver; import com.atlassian.sal.api.user.UserManager; import com.atlassian.sal.api.user.UserProfile; +import com.atlassian.velocity.htmlsafe.HtmlSafe; import com.baloise.confluence.digitalsignature.ContextHelper; import com.baloise.confluence.digitalsignature.Markdown; import com.baloise.confluence.digitalsignature.Signature; +import lombok.RequiredArgsConstructor; import org.apache.velocity.tools.generic.DateTool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; +import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -57,156 +53,140 @@ @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Scanned +@RequiredArgsConstructor public class DigitalSigatureService { - private static final Logger log = LoggerFactory.getLogger(DigitalSigatureService.class); - private final BandanaManager bandanaManager; - private final SettingsManager settingsManager; - private final UserManager userManager; - private final LocalNotificationService notificationService; - private final MailServerManager mailServerManager; - private final ContextHelper contextHelper = new ContextHelper(); - private final transient Markdown markdown = new Markdown(); - private final PageManager pageManager; - private I18nResolver i18nResolver; - - public DigitalSigatureService( - @ComponentImport BandanaManager bandanaManager, - @ComponentImport SettingsManager settingsManager, - @ComponentImport UserManager userManager, - @ComponentImport LocalNotificationService notificationService, - @ComponentImport MailServerManager mailServerManager, - @ComponentImport PageManager pageManager, - @ComponentImport I18nResolver i18nResolver - ) { - this.settingsManager = settingsManager; - this.bandanaManager = bandanaManager; - this.notificationService = notificationService; - this.userManager = userManager; - this.mailServerManager = mailServerManager; - this.pageManager = pageManager; - this.i18nResolver = i18nResolver; + private static final Logger log = LoggerFactory.getLogger(DigitalSigatureService.class); + private final BandanaManager bandanaManager; + private final SettingsManager settingsManager; + private final UserManager userManager; + private final LocalNotificationService notificationService; + private final MailServerManager mailServerManager; + private final PageManager pageManager; + private final I18nResolver i18nResolver; + private final ContextHelper contextHelper = new ContextHelper(); + private final transient Markdown markdown = new Markdown(); + + @GET + @Path("sign") + public Response sign(@QueryParam("key") final String key, @Context UriInfo uriInfo) { + ConfluenceUser confluenceUser = AuthenticatedUserThreadLocal.get(); + String userName = confluenceUser.getName(); + Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); + + if (!signature.sign(userName)) { + status(Response.Status.BAD_REQUEST) + .entity(i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.error.badUser", userName, key)) + .type(MediaType.TEXT_PLAIN) + .build(); } + bandanaManager.setValue(GLOBAL_CONTEXT, key, signature); - @GET - @Path("sign") - public Response sign(@QueryParam("key") final String key, @Context UriInfo uriInfo) { - ConfluenceUser confluenceUser = AuthenticatedUserThreadLocal.get(); - String userName = confluenceUser.getName(); - Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); - if (!signature.sign(userName)) { - status(Response.Status.BAD_REQUEST) - .entity(i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.error.badUser", userName, key)) - .type(MediaType.TEXT_PLAIN) - .build(); - } - bandanaManager.setValue(GLOBAL_CONTEXT, key, signature); - - String baseUrl = settingsManager.getGlobalSettings().getBaseUrl(); - for (String notifiedUser : signature.getNotify()) { - notify(notifiedUser, confluenceUser, signature, baseUrl); - } - Page parentPage = pageManager.getPage(signature.getPageId()); - Page protectedPage = pageManager.getPage(parentPage.getSpaceKey(), signature.getProtectedKey()); - if (protectedPage != null) { - protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, confluenceUser)); - pageManager.saveContentEntity(protectedPage, null); - } - URI pageUri = create(settingsManager.getGlobalSettings().getBaseUrl() + "/pages/viewpage.action?pageId=" + signature.getPageId()); - return temporaryRedirect(pageUri).build(); + String baseUrl = settingsManager.getGlobalSettings().getBaseUrl(); + for (String notifiedUser : signature.getNotify()) { + notify(notifiedUser, confluenceUser, signature, baseUrl); } - - private void notify(final String notifiedUser, ConfluenceUser signedUser, final Signature signature, final String baseUrl) { - try { - UserProfile notifiedUserProfile = contextHelper.getProfileNotNull(userManager, notifiedUser); - - String user = format("%s", - baseUrl + "/display/~" + signedUser.getName(), - signedUser.getFullName() - ); - String document = format("%s", - baseUrl + "/pages/viewpage.action?pageId=" + signature.getPageId(), - signature.getTitle() - ); - String html = i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.message.hasSignedShort", user, document); - if (signature.isMaxSignaturesReached()) { - html = html + "
" + i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.warning.maxSignaturesReached", signature.getMaxSignatures()); - } - String titleText = i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.message.hasSignedShort", signedUser.getFullName(), signature.getTitle()); - - notificationService.createOrUpdate(notifiedUser, new NotificationBuilder() - .application(PLUGIN_KEY) // a unique key that identifies your plugin - .title(titleText) - .itemTitle(titleText) - .description(html) - .groupingId(PLUGIN_KEY + "-signature") // a key to aggregate notifications - .createNotification()).get(); - - SMTPMailServer mailServer = mailServerManager.getDefaultSMTPMailServer(); - - if (mailServer == null) { - log.warn("No default SMTP server found -> no signature notification sent."); - } else if (!contextHelper.hasEmail(notifiedUserProfile)) { - log.warn(notifiedUser + " is to be notified but has no email address. Skipping email notification"); - } else { - mailServer.send( - new Email(notifiedUserProfile.getEmail()) - .setSubject(titleText) - .setBody(html) - .setMimeType("text/html") - ); - } - } catch (IllegalArgumentException | InterruptedException | MailException | ExecutionException e) { - log.error("Could not send notification to " + notifiedUser, e); - } - } - - @GET - @Path("export") - @Produces("text/html; charset=UTF-8") - @HtmlSafe - public String export(@QueryParam("key") final String key) { - Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); - - Map signed = contextHelper.getProfiles(userManager, signature.getSignatures().keySet()); - Map missing = contextHelper.getProfiles(userManager, signature.getMissingSignatures()); - - Map context = defaultVelocityContext(); - context.put("markdown", markdown); - context.put("orderedSignatures", contextHelper.getOrderedSignatures(signature)); - context.put("orderedMissingSignatureProfiles", contextHelper.getOrderedProfiles(userManager, signature.getMissingSignatures())); - context.put("profiles", contextHelper.union(signed, missing)); - context.put("signature", signature); - context.put("currentDate", new Date()); - context.put("date", new DateTool()); - - return getRenderedTemplate("templates/export.vm", context); + Page parentPage = pageManager.getPage(signature.getPageId()); + Page protectedPage = pageManager.getPage(parentPage.getSpaceKey(), signature.getProtectedKey()); + if (protectedPage != null) { + protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, confluenceUser)); + pageManager.saveContentEntity(protectedPage, null); } - - @GET - @Path("emails") - @Produces("text/html; charset=UTF-8") - public Response emails(@QueryParam("key") final String key, @QueryParam("signed") final boolean signed, @QueryParam("emailOnly") final boolean emailOnly, @Context UriInfo uriInfo) { - Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); - Map profiles = contextHelper.getProfiles(userManager, signed ? signature.getSignatures().keySet() : signature.getMissingSignatures()); - - Map context = defaultVelocityContext(); - context.put("signature", signature); - String signatureText = format("%s ( %s )", signature.getTitle(), signature.getHash()); - String rawTemplate = signed ? - i18nResolver.getRawText("com.baloise.confluence.digital-signature.signature.service.message.signedUsersEmails") : - i18nResolver.getRawText("com.baloise.confluence.digital-signature.signature.service.message.unsignedUsersEmails"); - context.put("signedOrNotWithHtml", MessageFormat.format(rawTemplate, "", "", signatureText)); - context.put("withNamesChecked", emailOnly ? "" : "checked"); - context.put("signedChecked", signed ? "checked" : ""); - context.put("toggleWithNamesURL", uriInfo.getRequestUriBuilder().replaceQueryParam("emailOnly", !emailOnly).build()); - context.put("toggleSignedURL", uriInfo.getRequestUriBuilder().replaceQueryParam("signed", !signed).build()); - Function mapping = p -> (emailOnly ? p.getEmail() : contextHelper.mailTo(p)).trim(); - context.put("emails", profiles.values().stream() - .filter(contextHelper::hasEmail) - .map(mapping).collect(toList())); - - context.put("currentDate", new Date()); - context.put("date", new DateTool()); - return Response.ok(getRenderedTemplate("templates/email.vm", context)).build(); + URI pageUri = create(settingsManager.getGlobalSettings().getBaseUrl() + "/pages/viewpage.action?pageId=" + signature.getPageId()); + return temporaryRedirect(pageUri).build(); + } + + private void notify(final String notifiedUser, ConfluenceUser signedUser, final Signature signature, final String baseUrl) { + try { + UserProfile notifiedUserProfile = contextHelper.getProfileNotNull(userManager, notifiedUser); + + String user = format("%s", + baseUrl + "/display/~" + signedUser.getName(), + signedUser.getFullName() + ); + String document = format("%s", + baseUrl + "/pages/viewpage.action?pageId=" + signature.getPageId(), + signature.getTitle() + ); + String html = i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.message.hasSignedShort", user, document); + if (signature.isMaxSignaturesReached()) { + html = html + "
" + i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.warning.maxSignaturesReached", signature.getMaxSignatures()); + } + String titleText = i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.message.hasSignedShort", signedUser.getFullName(), signature.getTitle()); + + notificationService.createOrUpdate(notifiedUser, new NotificationBuilder() + .application(PLUGIN_KEY) // a unique key that identifies your plugin + .title(titleText) + .itemTitle(titleText) + .description(html) + .groupingId(PLUGIN_KEY + "-signature") // a key to aggregate notifications + .createNotification()).get(); + + SMTPMailServer mailServer = mailServerManager.getDefaultSMTPMailServer(); + + if (mailServer == null) { + log.warn("No default SMTP server found -> no signature notification sent."); + } else if (!contextHelper.hasEmail(notifiedUserProfile)) { + log.warn(notifiedUser + " is to be notified but has no email address. Skipping email notification"); + } else { + mailServer.send( + new Email(notifiedUserProfile.getEmail()) + .setSubject(titleText) + .setBody(html) + .setMimeType("text/html") + ); + } + } catch (IllegalArgumentException | InterruptedException | MailException | ExecutionException e) { + log.error("Could not send notification to " + notifiedUser, e); } + } + + @GET + @Path("export") + @Produces("text/html; charset=UTF-8") + @HtmlSafe + public String export(@QueryParam("key") final String key) { + Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); + + Map signed = contextHelper.getProfiles(userManager, signature.getSignatures().keySet()); + Map missing = contextHelper.getProfiles(userManager, signature.getMissingSignatures()); + + Map context = defaultVelocityContext(); + context.put("markdown", markdown); + context.put("orderedSignatures", contextHelper.getOrderedSignatures(signature)); + context.put("orderedMissingSignatureProfiles", contextHelper.getOrderedProfiles(userManager, signature.getMissingSignatures())); + context.put("profiles", contextHelper.union(signed, missing)); + context.put("signature", signature); + context.put("currentDate", new Date()); + context.put("date", new DateTool()); + + return getRenderedTemplate("templates/export.vm", context); + } + + @GET + @Path("emails") + @Produces("text/html; charset=UTF-8") + public Response emails(@QueryParam("key") final String key, @QueryParam("signed") final boolean signed, @QueryParam("emailOnly") final boolean emailOnly, @Context UriInfo uriInfo) { + Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); + Map profiles = contextHelper.getProfiles(userManager, signed ? signature.getSignatures().keySet() : signature.getMissingSignatures()); + + Map context = defaultVelocityContext(); + context.put("signature", signature); + String signatureText = format("%s ( %s )", signature.getTitle(), signature.getHash()); + String rawTemplate = signed ? + i18nResolver.getRawText("com.baloise.confluence.digital-signature.signature.service.message.signedUsersEmails") : + i18nResolver.getRawText("com.baloise.confluence.digital-signature.signature.service.message.unsignedUsersEmails"); + context.put("signedOrNotWithHtml", MessageFormat.format(rawTemplate, "", "", signatureText)); + context.put("withNamesChecked", emailOnly ? "" : "checked"); + context.put("signedChecked", signed ? "checked" : ""); + context.put("toggleWithNamesURL", uriInfo.getRequestUriBuilder().replaceQueryParam("emailOnly", !emailOnly).build()); + context.put("toggleSignedURL", uriInfo.getRequestUriBuilder().replaceQueryParam("signed", !signed).build()); + Function mapping = p -> (emailOnly ? p.getEmail() : contextHelper.mailTo(p)).trim(); + context.put("emails", profiles.values().stream() + .filter(contextHelper::hasEmail) + .map(mapping).collect(toList())); + + context.put("currentDate", new Date()); + context.put("date", new DateTool()); + return Response.ok(getRenderedTemplate("templates/email.vm", context)).build(); + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/sal/DummyProfile.java b/src/main/java/com/baloise/confluence/digitalsignature/sal/DummyProfile.java index ac26213..30533b0 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/sal/DummyProfile.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/sal/DummyProfile.java @@ -7,45 +7,44 @@ public class DummyProfile implements UserProfile { - private String userKey; - - public DummyProfile(String userKey) { - this.userKey = userKey; - } - - @Override - public UserKey getUserKey() { - return new UserKey(userKey); - } - - @Override - public String getUsername() { - return userKey; - } - - @Override - public String getFullName() { - return userKey; - } - - @Override - public String getEmail() { - return ""; - } - - @Override - public URI getProfilePictureUri(int width, int height) { - return null; - } - - @Override - public URI getProfilePictureUri() { - return null; - } - - @Override - public URI getProfilePageUri() { - return null; - } - + private final String userKey; + + public DummyProfile(String userKey) { + this.userKey = userKey; + } + + @Override + public UserKey getUserKey() { + return new UserKey(userKey); + } + + @Override + public String getUsername() { + return userKey; + } + + @Override + public String getFullName() { + return userKey; + } + + @Override + public String getEmail() { + return ""; + } + + @Override + public URI getProfilePictureUri(int width, int height) { + return null; + } + + @Override + public URI getProfilePictureUri() { + return null; + } + + @Override + public URI getProfilePageUri() { + return null; + } } diff --git a/src/main/resources/META-INF/spring/plugin-context.xml b/src/main/resources/META-INF/spring/plugin-context.xml index 8f0fba7..faba45e 100644 --- a/src/main/resources/META-INF/spring/plugin-context.xml +++ b/src/main/resources/META-INF/spring/plugin-context.xml @@ -1,7 +1,7 @@ - 0) { - let shownSignees = Math.min(signedList.length, Math.ceil(remainingCount / 2)); - remainingCount = remainingCount - shownSignees; - for (let i = 0; i < signedList.length - shownSignees; i++) { - AJS.$(signedList[i]).hide(); - isSomethingHidden = true; - } + const signedList = $ul.find("li.signeelist-signed"); + const missingList = $ul.find("li.signeelist-missing"); + + let remainingCount = limit; + + let isSomethingHidden = false; + if (signedList.length > 0) { + let shownSignees = Math.min(signedList.length, Math.ceil(remainingCount / 2)); + remainingCount = remainingCount - shownSignees; + for (let i = 0; i < signedList.length - shownSignees; i++) { + AJS.$(signedList[i]).hide(); + isSomethingHidden = true; } + } - for (let i = 0; i < missingList.length - remainingCount; i++) { - AJS.$(missingList[i]).hide(); - isSomethingHidden = true; - } + for (let i = 0; i < missingList.length - remainingCount; i++) { + AJS.$(missingList[i]).hide(); + isSomethingHidden = true; + } - return isSomethingHidden; + return isSomethingHidden; } function showAllElements($ul) { - $ul.find("li").show(); - $ul.siblings("a.show-all").remove(); + $ul.find("li").show(); + $ul.siblings("a.show-all").remove(); } function bindCollapse(ul, limit, showMore) { - if (limit < 0) { - return; - } - - let $ul = AJS.$(ul); - - if (hideElements($ul, limit)) { - $ul.after("" + showMore + ""); - $ul.siblings("a.show-all").bind("click", function () { - showAllElements($ul); - }); - } + if (limit < 0) { + return; + } + + let $ul = AJS.$(ul); + + if (hideElements($ul, limit)) { + $ul.after("" + showMore + ""); + $ul.siblings("a.show-all").bind("click", function () { + showAllElements($ul); + }); + } } diff --git a/src/main/resources/templates/export.vm b/src/main/resources/templates/export.vm index dae8695..c20770c 100644 --- a/src/main/resources/templates/export.vm +++ b/src/main/resources/templates/export.vm @@ -1,12 +1,12 @@ #set( $dateFormatter = $action.getDateFormatter())

$signature.getTitle()

#set($bodyWithHtml = $markdown.toHTML($signature.getBody())) @@ -15,18 +15,18 @@ #foreach ($date2userName in $orderedSignatures) #set( $userName = $date2userName.key) #set( $profile = $profiles.get($userName)) - - $dateFormatter.formatDateTime($date2userName.value) - $profile.getFullName() - $profile.getEmail() - + + $dateFormatter.formatDateTime($date2userName.value) + $profile.getFullName() + $profile.getEmail() + #end #foreach( $profile in $orderedMissingSignatureProfiles) - - - $profile.getFullName() - $profile.getEmail() - + + + $profile.getFullName() + $profile.getEmail() + #end diff --git a/src/main/resources/templates/macro.vm b/src/main/resources/templates/macro.vm index be6ef39..77f2705 100644 --- a/src/main/resources/templates/macro.vm +++ b/src/main/resources/templates/macro.vm @@ -5,81 +5,81 @@ #set($macroId = $signature.getKey().replace("signature.", "")) #if($panel)
-
$title   - +
#else - $title + $title #end #set($bodyWithHtml = $markdown.toHTML($signature.getBody())) -

$bodyWithHtml

-
    - #foreach ($date2userName in $orderedSignatures) - #set( $userName = $date2userName.key) - #set( $profile = $profiles.get($userName)) -
  • - $dateFormatter.formatDateTime($date2userName.value) - $profile.getFullName() -
  • - #end - #foreach( $profile in $orderedMissingSignatureProfiles) -
  • - $profile.getFullName() -
  • - #end -
- #if($signAs) -
-
- -
-
- #end +

$bodyWithHtml

+
    + #foreach ($date2userName in $orderedSignatures) + #set( $userName = $date2userName.key) + #set( $profile = $profiles.get($userName)) +
  • + $dateFormatter.formatDateTime($date2userName.value) - $profile.getFullName() +
  • + #end + #foreach( $profile in $orderedMissingSignatureProfiles) +
  • + $profile.getFullName() +
  • + #end +
+#if($signAs) +
+
+ +
+
+#end #if($panel)
#end diff --git a/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java b/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java index 44bb605..27c6caf 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java @@ -1,24 +1,22 @@ package com.baloise.confluence.digitalsignature; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Test; - import com.atlassian.confluence.setup.BootstrapManager; import com.atlassian.sal.api.user.UserProfile; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class DigitalSignatureMacroTest { - +class DigitalSignatureMacroTest { private final Signature signature = new Signature(1, "test", "title"); private final BootstrapManager bootstrapManager = mock(BootstrapManager.class); @Test - public void getMailtoLong() { + void getMailtoLong() { DigitalSignatureMacro macro = new DigitalSignatureMacro(null, null, null, null, null, null, null); List profiles = new ArrayList<>(); UserProfile profile = mock(UserProfile.class); @@ -27,12 +25,14 @@ public void getMailtoLong() { for (int i = 0; i < 20; i++) { profiles.add(profile); } + String mailto = macro.getMailto(profiles, "Subject", true, null); + assertEquals("mailto:heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com,heinz.meier@meier.com?Subject=Subject", mailto); } @Test - public void getMailtoVeryLong() { + void getMailtoVeryLong() { when(bootstrapManager.getWebAppContextPath()).thenReturn("nirvana"); DigitalSignatureMacro macro = new DigitalSignatureMacro(null, null, bootstrapManager, null, null, null, null); @@ -43,19 +43,23 @@ public void getMailtoVeryLong() { for (int i = 0; i < 200; i++) { profiles.add(profile); } + String mailto = macro.getMailto(profiles, "Subject", true, signature); + assertEquals("nirvana/rest/signature/1.0/emails?key=signature.3224a4d6bba68cd0ece9b64252f8bf5677e24cf6b7c5f543e3176d419d34d517&signed=true", mailto); } @Test - public void getMailtoShort() { + void getMailtoShort() { DigitalSignatureMacro macro = new DigitalSignatureMacro(null, null, null, null, null, null, null); List profiles = new ArrayList<>(); UserProfile profile = mock(UserProfile.class); when(profile.getFullName()).thenReturn("Heinz Meier"); when(profile.getEmail()).thenReturn("heinz.meier@meier.com"); profiles.add(profile); + String mailto = macro.getMailto(profiles, "Subject", true, null); + assertEquals("mailto:Heinz Meier?Subject=Subject", mailto); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/InheritSignersTest.java b/src/test/java/com/baloise/confluence/digitalsignature/InheritSignersTest.java index 3d60ce5..9d124bc 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/InheritSignersTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/InheritSignersTest.java @@ -1,25 +1,24 @@ package com.baloise.confluence.digitalsignature; -import org.junit.Test; -import static com.baloise.confluence.digitalsignature.InheritSigners.NONE; -import static com.baloise.confluence.digitalsignature.InheritSigners.READERS_ONLY; -import static com.baloise.confluence.digitalsignature.InheritSigners.ofValue; -import static org.junit.Assert.assertEquals; +import org.junit.jupiter.api.Test; -public class InheritSignersTest { +import static com.baloise.confluence.digitalsignature.InheritSigners.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InheritSignersTest { @Test - public void testOfValueReadersOnly() { + void testOfValueReadersOnly() { assertEquals(READERS_ONLY, ofValue("readers only")); } @Test - public void testOfValueNoneNull() { + void testOfValueNoneNull() { assertEquals(NONE, ofValue(null)); } @Test - public void testOfValueNoneIllegalArgument() { + void testOfValueNoneIllegalArgument() { assertEquals(NONE, ofValue("asdasd")); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/MarkdownTest.java b/src/test/java/com/baloise/confluence/digitalsignature/MarkdownTest.java index e87b7a6..575b2e8 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/MarkdownTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/MarkdownTest.java @@ -1,33 +1,36 @@ package com.baloise.confluence.digitalsignature; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.URISyntaxException; import static java.nio.file.Files.readAllLines; import static java.nio.file.Paths.get; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class MarkdownTest { - private Markdown markdown; +class MarkdownTest { + private Markdown markdown; - @Before - public void setUp() { - markdown = new Markdown(); - } + @BeforeEach + void setUp() { + markdown = new Markdown(); + } - @Test - public void testToHTML() throws Exception { - assertEquals("

This is Sparta

\n", markdown.toHTML("This is *Sparta*")); - assertEquals("

Link

\n", markdown.toHTML("[Link](http://a.com)")); - assertEquals("

\n", markdown.toHTML("![Image](http://url/a.png)")); - assertEquals("

<b></b>

\n", markdown.toHTML("")); - assertEquals(readResource("commonmark.html").trim(), markdown.toHTML(readResource("commonmark.md")).trim()); - } + @Test + void testToHTML() { + assertAll( + () -> assertEquals("

This is Sparta

\n", markdown.toHTML("This is *Sparta*")), + () -> assertEquals("

Link

\n", markdown.toHTML("[Link](http://a.com)")), + () -> assertEquals("

\n", markdown.toHTML("![Image](http://url/a.png)")), + () -> assertEquals("

<b></b>

\n", markdown.toHTML("")), + () -> assertEquals(readResource("commonmark.html").trim(), markdown.toHTML(readResource("commonmark.md")).trim()) + ); + } - private String readResource(String name) throws IOException, URISyntaxException { - return String.join("\n", readAllLines(get(getClass().getResource("/" + name).toURI()))); - } + private String readResource(String name) throws IOException, URISyntaxException { + return String.join("\n", readAllLines(get(getClass().getResource("/" + name).toURI()))); + } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/MessageFormatTest.java b/src/test/java/com/baloise/confluence/digitalsignature/MessageFormatTest.java index 4d2dde1..5470115 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/MessageFormatTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/MessageFormatTest.java @@ -1,19 +1,23 @@ package com.baloise.confluence.digitalsignature; -import java.text.MessageFormat; +import org.junit.jupiter.api.Test; -import org.junit.Test; +import java.text.MessageFormat; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class MessageFormatTest { +class MessageFormatTest { @Test - public void test() { + void testFormat_inOrder() { String rawTemplate = "Email addresses of users who {0}signed{1} {2}"; String actual = MessageFormat.format(rawTemplate, "", "", "#123"); assertEquals("Email addresses of users who signed #123", actual); - rawTemplate = "{2} was {0}signed{1}"; - actual = MessageFormat.format(rawTemplate, "", "", "#123"); + } + + @Test + void testFormat_outOfOrder() { + String rawTemplate = "{2} was {0}signed{1}"; + String actual = MessageFormat.format(rawTemplate, "", "", "#123"); assertEquals("#123 was signed", actual); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java index 165ce7f..80efb2a 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java @@ -1,6 +1,7 @@ package com.baloise.confluence.digitalsignature; -import org.junit.Test; + +import org.junit.jupiter.api.Test; import java.io.FileOutputStream; import java.io.IOException; @@ -8,34 +9,37 @@ import java.io.ObjectOutputStream; import java.util.Date; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class SignatureSerialisationTest { - @Test - public void deserialise() throws IOException, ClassNotFoundException { - ObjectInputStream in = new ObjectInputStream(getClass().getResourceAsStream("/signature.ser")); - Signature signature = (Signature) in.readObject(); - in.close(); - assertEquals("signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3", signature.getKey()); - assertEquals("[missing1, missing2]", signature.getMissingSignatures().toString()); - assertEquals(1, signature.getSignatures().size()); - assertTrue(signature.getSignatures().containsKey("signed1")); - assertEquals(9999, signature.getSignatures().get("signed1").getTime()); - } - - @Test - public void serialise() throws IOException, ClassNotFoundException { - Signature signature = new Signature(123L, "body", "title"); - signature.getNotify().add("notify1"); - signature.getMissingSignatures().add("missing1"); - signature.getMissingSignatures().add("missing2"); - signature.getSignatures().put("signed1", new Date(9999)); - ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("src/test/resources/signature-test.ser")); - out.writeObject(signature); - out.close(); - - ObjectInputStream in = new ObjectInputStream(this.getClass().getResourceAsStream("/signature.ser")); - assertEquals(signature, in.readObject()); - } +import static org.junit.jupiter.api.Assertions.*; + + +class SignatureSerialisationTest { + @Test + void deserialize() throws IOException, ClassNotFoundException { + ObjectInputStream in = new ObjectInputStream(getClass().getResourceAsStream("/signature.ser")); + Signature signature = (Signature) in.readObject(); + in.close(); + + assertAll( + () -> assertEquals("signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3", signature.getKey()), + () -> assertEquals("[missing1, missing2]", signature.getMissingSignatures().toString()), + () -> assertEquals(1, signature.getSignatures().size()), + () -> assertTrue(signature.getSignatures().containsKey("signed1")), + () -> assertEquals(9999, signature.getSignatures().get("signed1").getTime()) + ); + } + + @Test + void serialize() throws IOException, ClassNotFoundException { + Signature signature = new Signature(123L, "body", "title"); + signature.getNotify().add("notify1"); + signature.getMissingSignatures().add("missing1"); + signature.getMissingSignatures().add("missing2"); + signature.getSignatures().put("signed1", new Date(9999)); + ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("src/test/resources/signature-test.ser")); + out.writeObject(signature); + out.close(); + + ObjectInputStream in = new ObjectInputStream(this.getClass().getResourceAsStream("/signature.ser")); + assertEquals(signature, in.readObject()); + } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java index fe281d5..76b6711 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java @@ -1,19 +1,20 @@ package com.baloise.confluence.digitalsignature; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -public class SignatureTest { +import static org.junit.jupiter.api.Assertions.*; +class SignatureTest { @Test - public void testClone() throws Exception { + void testClone() throws Exception { Signature signature = new Signature(999, "title", "body"); signature.getMissingSignatures().add("Hans"); Signature cloned = signature.clone(); - assertFalse(signature == cloned); - assertEquals(signature, cloned); - assertEquals("Hans", cloned.getMissingSignatures().iterator().next()); + + assertAll( + () -> assertNotSame(signature, cloned), + () -> assertEquals(signature, cloned), + () -> assertEquals("Hans", cloned.getMissingSignatures().iterator().next()) + ); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/TemplatesTest.java b/src/test/java/com/baloise/confluence/digitalsignature/TemplatesTest.java index e4930a3..d2112e3 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/TemplatesTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/TemplatesTest.java @@ -1,46 +1,46 @@ package com.baloise.confluence.digitalsignature; import org.apache.velocity.VelocityContext; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.BufferedWriter; import java.io.StringWriter; import java.io.Writer; import static org.apache.velocity.app.Velocity.mergeTemplate; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class TemplatesTest { - private static String normalize(String input) { - return input.replaceAll("[\n\r]", "") - .replaceAll(" +", " ") - .replaceAll("> <", "><") - .trim(); - } +class TemplatesTest { + private static String normalize(String input) { + return input.replaceAll("[\n\r]", "") + .replaceAll(" +", " ") + .replaceAll("> <", "><") + .trim(); + } - @Test - public void testMacroVm() throws Exception { - StringWriter sw = new StringWriter(); - //lets use BufferedWriter for better performance: - Writer writer = new BufferedWriter(sw); - VelocityContext context = new VelocityContext(); - //add your parameters to context - mergeTemplate("src/main/resources/templates/macro.vm", "UTF-8", context, writer); - writer.flush(); - String expected = "#requireResource(\"com.baloise.confluence.digital-signature:digital-signature-resources\") $title

$bodyWithHtml

    "; - assertEquals(expected, normalize(sw.toString())); - } + @Test + void testMacroVm() throws Exception { + StringWriter sw = new StringWriter(); + //lets use BufferedWriter for better performance: + Writer writer = new BufferedWriter(sw); + VelocityContext context = new VelocityContext(); + //add your parameters to context + mergeTemplate("src/main/resources/templates/macro.vm", "UTF-8", context, writer); + writer.flush(); + String expected = "#requireResource(\"com.baloise.confluence.digital-signature:digital-signature-resources\") $title

    $bodyWithHtml

      "; + assertEquals(expected, normalize(sw.toString())); + } - @Test - public void testExportVm() throws Exception { - StringWriter sw = new StringWriter(); - //lets use BufferedWriter for better performance: - Writer writer = new BufferedWriter(sw); - VelocityContext context = new VelocityContext(); - //add your parameters to context - mergeTemplate("src/main/resources/templates/export.vm", "UTF-8", context, writer); - writer.flush(); - String expected = "

      $signature.getTitle()

      $bodyWithHtml

      "; - assertEquals(expected, normalize(sw.toString())); - } + @Test + void testExportVm() throws Exception { + StringWriter sw = new StringWriter(); + //lets use BufferedWriter for better performance: + Writer writer = new BufferedWriter(sw); + VelocityContext context = new VelocityContext(); + //add your parameters to context + mergeTemplate("src/main/resources/templates/export.vm", "UTF-8", context, writer); + writer.flush(); + String expected = "

      $signature.getTitle()

      $bodyWithHtml

      "; + assertEquals(expected, normalize(sw.toString())); + } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java b/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java index 5c731c8..c3f625e 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java @@ -1,31 +1,31 @@ package com.baloise.confluence.digitalsignature; import com.atlassian.sal.api.user.UserProfile; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import java.util.SortedSet; import java.util.TreeSet; -import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; -public class UserProfileByNameTest { +class UserProfileByNameTest { + @Test + void testCompare() { + UserProfile profile1 = Mockito.mock(UserProfile.class); + when(profile1.getFullName()).thenReturn("Heinz Meier"); + when(profile1.getEmail()).thenReturn("heinz.meier@meier.com"); + when(profile1.toString()).thenReturn("Heinz Meier"); + UserProfile profile2 = Mockito.mock(UserProfile.class); + when(profile2.getFullName()).thenReturn("Abraham Aebischer"); + when(profile2.getEmail()).thenReturn("Abraham Aebischer@meier.com"); + when(profile2.toString()).thenReturn("Abraham Aebischer"); + SortedSet profiles = new TreeSet<>(new UserProfileByName()); + profiles.add(profile1); + profiles.add(profile2); + profiles.add(profile1); - @Test - public void testCompare() { - UserProfile profile1 = Mockito.mock(UserProfile.class); - when(profile1.getFullName()).thenReturn("Heinz Meier"); - when(profile1.getEmail()).thenReturn("heinz.meier@meier.com"); - when(profile1.toString()).thenReturn("Heinz Meier"); - UserProfile profile2 = Mockito.mock(UserProfile.class); - when(profile2.getFullName()).thenReturn("Abraham Aebischer"); - when(profile2.getEmail()).thenReturn("Abraham Aebischer@meier.com"); - when(profile2.toString()).thenReturn("Abraham Aebischer"); - SortedSet profiles = new TreeSet<>(new UserProfileByName()); - profiles.add(profile1); - profiles.add(profile2); - profiles.add(profile1); - assertEquals("[Abraham Aebischer, Heinz Meier]", profiles.toString()); - } + Assertions.assertEquals("[Abraham Aebischer, Heinz Meier]", profiles.toString()); + } } diff --git a/src/test/java/ut/com/baloise/confluence/digitalsignature/MyComponentUnitTest.java b/src/test/java/ut/com/baloise/confluence/digitalsignature/MyComponentUnitTest.java index b3e44db..6e66e1c 100644 --- a/src/test/java/ut/com/baloise/confluence/digitalsignature/MyComponentUnitTest.java +++ b/src/test/java/ut/com/baloise/confluence/digitalsignature/MyComponentUnitTest.java @@ -1,15 +1,14 @@ package ut.com.baloise.confluence.digitalsignature; -import org.junit.Test; import com.baloise.confluence.digitalsignature.api.DigitalSignatureComponent; import com.baloise.confluence.digitalsignature.impl.DigitalSignatureComponentImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; - -public class MyComponentUnitTest { - @Test - public void testMyName() { - DigitalSignatureComponent component = new DigitalSignatureComponentImpl(null); - assertEquals("names do not match!", "digitalSignatureComponent", component.getName()); - } +class MyComponentUnitTest { + @Test + void testMyName() { + DigitalSignatureComponent component = new DigitalSignatureComponentImpl(null); + Assertions.assertEquals("digitalSignatureComponent", component.getName(), "names do not match!"); + } } diff --git a/src/test/resources/atlassian-plugin.xml b/src/test/resources/atlassian-plugin.xml index ccf2a09..140bc08 100644 --- a/src/test/resources/atlassian-plugin.xml +++ b/src/test/resources/atlassian-plugin.xml @@ -2,13 +2,13 @@ ${project.description} ${project.version} - + - + - - - \ No newline at end of file + + From b86a2cf4e83a8c5a76a045665bbf86d86b5aa3d1 Mon Sep 17 00:00:00 2001 From: tiliavir Date: Sat, 5 Feb 2022 13:59:54 +0100 Subject: [PATCH 02/16] #82: Bandana only used to store Strings in future - BandanaManager.init() is called initially - removes workaround with clone - serialization of Signature to JSON using GSON --- .../DigitalSignatureMacro.java | 38 ++++++++++--- .../digitalsignature/Signature.java | 26 +++++---- .../DigitalSignatureMacroTest.java | 8 +-- .../digitalsignature/SignatureTest.java | 54 +++++++++++++++---- .../UserProfileByNameTest.java | 6 +-- 5 files changed, 96 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java index c6d2cff..6db12e7 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java @@ -1,5 +1,6 @@ package com.baloise.confluence.digitalsignature; +import com.atlassian.bandana.BandanaContext; import com.atlassian.bandana.BandanaManager; import com.atlassian.confluence.content.render.xhtml.ConversionContext; import com.atlassian.confluence.core.ContentEntityObject; @@ -57,7 +58,13 @@ public class DigitalSignatureMacro implements Macro { private final ContextHelper contextHelper = new ContextHelper(); @Autowired - public DigitalSignatureMacro(@ComponentImport BandanaManager bandanaManager, @ComponentImport UserManager userManager, @ComponentImport BootstrapManager bootstrapManager, @ComponentImport PageManager pageManager, @ComponentImport PermissionManager permissionManager, @ComponentImport GroupManager groupManager, @ComponentImport I18nResolver i18nResolver) { + public DigitalSignatureMacro(@ComponentImport BandanaManager bandanaManager, + @ComponentImport UserManager userManager, + @ComponentImport BootstrapManager bootstrapManager, + @ComponentImport PageManager pageManager, + @ComponentImport PermissionManager permissionManager, + @ComponentImport GroupManager groupManager, + @ComponentImport I18nResolver i18nResolver) { this.bandanaManager = bandanaManager; this.userManager = userManager; this.bootstrapManager = bootstrapManager; @@ -65,6 +72,8 @@ public DigitalSignatureMacro(@ComponentImport BandanaManager bandanaManager, @Co this.permissionManager = permissionManager; this.groupManager = groupManager; this.i18nResolver = i18nResolver; + + this.bandanaManager.init(); all.add("*"); } @@ -154,11 +163,6 @@ private void ensureProtectedPage(ConversionContext conversionContext, Page page, } private boolean hideSignatures(Map params, Signature signature, String currentUserName) { - try { - signature = signature.clone(); - } catch (CloneNotSupportedException e) { - throw new IllegalStateException(e); - } boolean pendingVisible = isVisible(signature, currentUserName, params.get("pendingVisible")); boolean signaturesVisible = isVisible(signature, currentUserName, params.get("signaturesVisible")); if (!pendingVisible) signature.setMissingSignatures(new TreeSet<>()); @@ -267,7 +271,7 @@ private Set getSet(Map params, String key) { } private Signature sync(Signature signature, Set signers) { - Signature loaded = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, signature.getKey()); + Signature loaded = fromBandana(GLOBAL_CONTEXT, signature.getKey()); if (loaded != null) { signature.setSignatures(loaded.getSignatures()); boolean save = false; @@ -303,7 +307,25 @@ private Signature sync(Signature signature, Set signers) { } private void save(Signature signature) { - if (signature.hasMissingSignatures()) bandanaManager.setValue(GLOBAL_CONTEXT, signature.getKey(), signature); + if (signature.hasMissingSignatures()) { + bandanaManager.setValue(GLOBAL_CONTEXT, signature.getKey(), signature.serialize()); + } + } + + Signature fromBandana(BandanaContext context, String key) { + Object value = this.bandanaManager.getValue(context, key); + if (value instanceof Signature) { + // required for downward compatibility - update for next time. + Signature signature = (Signature) value; + this.bandanaManager.setValue(context, key, signature.serialize()); + return signature; + } + + if (value instanceof String) { + return Signature.deserialize((String) value); + } + + throw new IllegalArgumentException("Cannot read value from Bandana."); } @Override diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index b987ed1..8871a88 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -1,20 +1,21 @@ package com.baloise.confluence.digitalsignature; +import com.google.gson.Gson; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; - import java.io.Serializable; import java.util.*; +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; + @Getter @Setter @NoArgsConstructor -public class Signature implements Serializable, Cloneable { +public class Signature implements Serializable { + public static final Gson GSON = new Gson(); private static final long serialVersionUID = 1L; - private String key = ""; private String hash = ""; private long pageId; @@ -30,8 +31,8 @@ public Signature(long pageId, String body, String title) { this.pageId = pageId; this.body = body; this.title = title == null ? "" : title; - hash = sha256Hex(pageId + ":" + title + ":" + body); - key = "signature." + hash; + this.hash = sha256Hex(pageId + ":" + title + ":" + body); + this.key = "signature." + hash; } public static boolean isPetitionMode(Set userGroups) { @@ -40,6 +41,14 @@ public static boolean isPetitionMode(Set userGroups) { && userGroups.iterator().next().trim().equals("*"); } + public static Signature deserialize(String serialization) { + return GSON.fromJson(serialization, Signature.class); + } + + public String serialize() { + return GSON.toJson(this, Signature.class); + } + public String getHash() { if (hash == null) { hash = getKey().replace("signature.", ""); @@ -118,9 +127,4 @@ public boolean isSignatory(String userName) { public boolean hasMissingSignatures() { return !isMaxSignaturesReached() && (isPetitionMode() || !getMissingSignatures().isEmpty()); } - - @Override - public Signature clone() throws CloneNotSupportedException { - return (Signature) super.clone(); - } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java b/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java index 27c6caf..8bebb10 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java @@ -1,5 +1,6 @@ package com.baloise.confluence.digitalsignature; +import com.atlassian.bandana.BandanaManager; import com.atlassian.confluence.setup.BootstrapManager; import com.atlassian.sal.api.user.UserProfile; import org.junit.jupiter.api.Test; @@ -14,10 +15,11 @@ class DigitalSignatureMacroTest { private final Signature signature = new Signature(1, "test", "title"); private final BootstrapManager bootstrapManager = mock(BootstrapManager.class); + private final BandanaManager bandana = mock(BandanaManager.class); @Test void getMailtoLong() { - DigitalSignatureMacro macro = new DigitalSignatureMacro(null, null, null, null, null, null, null); + DigitalSignatureMacro macro = new DigitalSignatureMacro(bandana, null, null, null, null, null, null); List profiles = new ArrayList<>(); UserProfile profile = mock(UserProfile.class); when(profile.getFullName()).thenReturn("Heinz Meier"); @@ -35,7 +37,7 @@ void getMailtoLong() { void getMailtoVeryLong() { when(bootstrapManager.getWebAppContextPath()).thenReturn("nirvana"); - DigitalSignatureMacro macro = new DigitalSignatureMacro(null, null, bootstrapManager, null, null, null, null); + DigitalSignatureMacro macro = new DigitalSignatureMacro(bandana, null, bootstrapManager, null, null, null, null); List profiles = new ArrayList<>(); UserProfile profile = mock(UserProfile.class); when(profile.getFullName()).thenReturn("Heinz Meier"); @@ -51,7 +53,7 @@ void getMailtoVeryLong() { @Test void getMailtoShort() { - DigitalSignatureMacro macro = new DigitalSignatureMacro(null, null, null, null, null, null, null); + DigitalSignatureMacro macro = new DigitalSignatureMacro(bandana, null, null, null, null, null, null); List profiles = new ArrayList<>(); UserProfile profile = mock(UserProfile.class); when(profile.getFullName()).thenReturn("Heinz Meier"); diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java index 76b6711..d85a89a 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java @@ -2,19 +2,51 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; class SignatureTest { @Test - void testClone() throws Exception { - Signature signature = new Signature(999, "title", "body"); - signature.getMissingSignatures().add("Hans"); - Signature cloned = signature.clone(); - - assertAll( - () -> assertNotSame(signature, cloned), - () -> assertEquals(signature, cloned), - () -> assertEquals("Hans", cloned.getMissingSignatures().iterator().next()) - ); + void serialize_empty() { + Signature signature = new Signature(); + + String json = signature.serialize(); + + assertEquals("{\"key\":\"\",\"hash\":\"\",\"pageId\":0,\"title\":\"\",\"body\":\"\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[],\"notify\":[]}", json); + } + + @Test + void serialize_initializedObject() { + Signature signature = new Signature(42L, "body text", "title text"); + signature.sign("max.mustermann"); + signature.setMissingSignatures(Set.of("max.muster")); + signature.setNotify(Set.of("max.meier")); + + String json = signature.serialize(); + + assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); + } + + @Test + void deserialize_empty() { + assertNull(Signature.deserialize(null)); + assertNull(Signature.deserialize("")); + } + + @Test + void serializeAndDeserialize() { + Signature signature = new Signature(42L, "body text", "title text"); + signature.sign("max.mustermann"); + signature.setMissingSignatures(Set.of("max.muster")); + signature.setNotify(Set.of("max.meier")); + + String json = signature.serialize(); + + Signature restoredSignature = Signature.deserialize(json); + + assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); + assertEquals(signature, restoredSignature); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java b/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java index c3f625e..226d99f 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/UserProfileByNameTest.java @@ -3,21 +3,21 @@ import com.atlassian.sal.api.user.UserProfile; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.util.SortedSet; import java.util.TreeSet; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class UserProfileByNameTest { @Test void testCompare() { - UserProfile profile1 = Mockito.mock(UserProfile.class); + UserProfile profile1 = mock(UserProfile.class); when(profile1.getFullName()).thenReturn("Heinz Meier"); when(profile1.getEmail()).thenReturn("heinz.meier@meier.com"); when(profile1.toString()).thenReturn("Heinz Meier"); - UserProfile profile2 = Mockito.mock(UserProfile.class); + UserProfile profile2 = mock(UserProfile.class); when(profile2.getFullName()).thenReturn("Abraham Aebischer"); when(profile2.getEmail()).thenReturn("Abraham Aebischer@meier.com"); when(profile2.toString()).thenReturn("Abraham Aebischer"); From d294ddaa0b5453c5c53fec15423226ef2c31c055 Mon Sep 17 00:00:00 2001 From: Markus Lindenmann Date: Sun, 13 Feb 2022 12:58:37 +0100 Subject: [PATCH 03/16] #82: Fixes issue with lombok in artifact --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 96d82d3..c84e0f0 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ org.projectlombok lombok 1.18.22 - compile + provided com.atlassian.plugin @@ -111,7 +111,6 @@ 2.0.0-alpha1 test - From 46f2b994fde62aaeda249b08b242a1cc973cf296 Mon Sep 17 00:00:00 2001 From: Markus Lindenmann Date: Sun, 13 Feb 2022 12:59:26 +0100 Subject: [PATCH 04/16] #82: removes section from README mentioning cache issues --- README.MD | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.MD b/README.MD index 5f68943..19c3c9b 100644 --- a/README.MD +++ b/README.MD @@ -25,8 +25,6 @@ the [Wiki...](https://github.com/baloise/digital-signature/wiki/Signature-Macro- ## Using Confluence Data Center Version Digital-signature can be used on Confluence Data Center, however it is not yet officially tested and approved. -When performing an update you might have to invalidate the plugin cache and restart Confluence. For details refer -to [#68](https://github.com/baloise/digital-signature/issues/68). ## Feature overview ### Insert / edit macro From 4777948ff0ad46096699fe4e9aa619f8889102bd Mon Sep 17 00:00:00 2001 From: Markus Lindenmann Date: Sun, 13 Feb 2022 13:00:12 +0100 Subject: [PATCH 05/16] #82: adds documentation for docker setup --- docs/docker.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/docker.md diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..cf77c84 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,14 @@ +Setup following a [tutorial from coffeetime.solutions]( http://coffeetime.solutions/run-atlassian-jira-and-confluence-with-postgresql-on-docker/#Overview_of_series_How_to_run_Jira_and_Confluence_behind_NGINX_reverse_proxy_on_Docker): + +```bash +docker run --name=confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:6.15.9 +docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres +``` + +Start confluence setup and configure Postgres: +- jdbc:postgresql://192.168.65.2:5432/postgres (`docker inspect postgres` to get ip address) +- user: postgres +- password: mysecretpassword (defined above) + +Skip tutorial +Create new space "Test" From 4bd5506e8cc2c689f067ab17236ac85b4726e2cd Mon Sep 17 00:00:00 2001 From: tiliavir Date: Tue, 15 Feb 2022 11:15:14 +0100 Subject: [PATCH 06/16] #77, #80: Fixes NPE and handles errors gracefully --- README.MD | 8 +- docs/docker.md | 2 +- .../DigitalSignatureMacro.java | 22 +---- .../digitalsignature/Signature.java | 45 ++++++++- ...vice.java => DigitalSignatureService.java} | 91 ++++++++++++++----- .../digitalsignature/SignatureTest.java | 90 ++++++++++++------ 6 files changed, 180 insertions(+), 78 deletions(-) rename src/main/java/com/baloise/confluence/digitalsignature/rest/{DigitalSigatureService.java => DigitalSignatureService.java} (71%) diff --git a/README.MD b/README.MD index 19c3c9b..a6b9e45 100644 --- a/README.MD +++ b/README.MD @@ -12,6 +12,12 @@ Allows confluence users to write contracts in a confluence macro which can be si - easily send email to signers of the contract - receive notifications, when your contract was signed +## ClassCastException issue +If you observe issues in the Macro resulting in a `ClassCastException` please update digital-signature to version 7.0.5, +clear the plugin cache (one last time) and restart confluence. + +For backroung information please refer to [#82](https://github.com/baloise/digital-signature/issues/82.) + ## Privacy Policy - We do not transfer or store any data outside your Atlassian product. - We have no access to any data you stored within your Atlassian product. @@ -43,7 +49,7 @@ Digital-signature can be used on Confluence Data Center, however it is not yet o ### Mail notification ![](./docs/img/report_email_export.png) -![](./docs/img/send_mail.png.png) +![](./docs/img/send_mail.png) ## Contribute Keep it simple: every contribution is welcome. Either if you report an issue, help on solving one, or contribute to the diff --git a/docs/docker.md b/docs/docker.md index cf77c84..82a33ae 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,7 +1,7 @@ Setup following a [tutorial from coffeetime.solutions]( http://coffeetime.solutions/run-atlassian-jira-and-confluence-with-postgresql-on-docker/#Overview_of_series_How_to_run_Jira_and_Confluence_behind_NGINX_reverse_proxy_on_Docker): ```bash -docker run --name=confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:6.15.9 +docker run --name=confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:latest docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres ``` diff --git a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java index 6db12e7..aaeb4a8 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java @@ -1,6 +1,5 @@ package com.baloise.confluence.digitalsignature; -import com.atlassian.bandana.BandanaContext; import com.atlassian.bandana.BandanaManager; import com.atlassian.confluence.content.render.xhtml.ConversionContext; import com.atlassian.confluence.core.ContentEntityObject; @@ -73,7 +72,6 @@ public DigitalSignatureMacro(@ComponentImport BandanaManager bandanaManager, this.groupManager = groupManager; this.i18nResolver = i18nResolver; - this.bandanaManager.init(); all.add("*"); } @@ -271,7 +269,7 @@ private Set getSet(Map params, String key) { } private Signature sync(Signature signature, Set signers) { - Signature loaded = fromBandana(GLOBAL_CONTEXT, signature.getKey()); + Signature loaded = Signature.fromBandana(this.bandanaManager, signature.getKey()); if (loaded != null) { signature.setSignatures(loaded.getSignatures()); boolean save = false; @@ -308,26 +306,10 @@ private Signature sync(Signature signature, Set signers) { private void save(Signature signature) { if (signature.hasMissingSignatures()) { - bandanaManager.setValue(GLOBAL_CONTEXT, signature.getKey(), signature.serialize()); + Signature.toBandana(bandanaManager, signature); } } - Signature fromBandana(BandanaContext context, String key) { - Object value = this.bandanaManager.getValue(context, key); - if (value instanceof Signature) { - // required for downward compatibility - update for next time. - Signature signature = (Signature) value; - this.bandanaManager.setValue(context, key, signature.serialize()); - return signature; - } - - if (value instanceof String) { - return Signature.deserialize((String) value); - } - - throw new IllegalArgumentException("Cannot read value from Bandana."); - } - @Override public BodyType getBodyType() { return BodyType.PLAIN_TEXT; diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index 8871a88..20448f0 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -1,15 +1,19 @@ package com.baloise.confluence.digitalsignature; +import com.atlassian.bandana.BandanaManager; import com.google.gson.Gson; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import java.io.Serializable; import java.util.*; +import static com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext.GLOBAL_CONTEXT; import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; +@Slf4j @Getter @Setter @NoArgsConstructor @@ -41,12 +45,47 @@ public static boolean isPetitionMode(Set userGroups) { && userGroups.iterator().next().trim().equals("*"); } - public static Signature deserialize(String serialization) { + String serialize() { + return GSON.toJson(this, Signature.class); + } + + static Signature deserialize(String serialization) { return GSON.fromJson(serialization, Signature.class); } - public String serialize() { - return GSON.toJson(this, Signature.class); + public static Signature fromBandana(BandanaManager mgr, String key) { + Object value = mgr.getValue(GLOBAL_CONTEXT, key); + + if (value == null) { + return null; + } + + if (value instanceof Signature) { + // required for downward compatibility - update for next time. + Signature signature = (Signature) value; + toBandana(mgr, key, signature); + return signature; + } + + if (value instanceof String) { + try { + return deserialize((String) value); + } catch (Exception e) { + log.error("Could not deserialize String value from Bandana", e); + return null; + } + } + + log.error("Could not deserialize {} value from Bandana", value.getClass().getName()); + return null; + } + + public static void toBandana(BandanaManager mgr, String key, Signature sig) { + mgr.setValue(GLOBAL_CONTEXT, key, sig.serialize()); + } + + public static void toBandana(BandanaManager mgr, Signature sig) { + toBandana(mgr, sig.getKey(), sig); } public String getHash() { diff --git a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java similarity index 71% rename from src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java rename to src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java index 58e447c..251458f 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSigatureService.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java @@ -13,6 +13,7 @@ import com.atlassian.mywork.model.NotificationBuilder; import com.atlassian.mywork.service.LocalNotificationService; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import com.atlassian.sal.api.message.I18nResolver; import com.atlassian.sal.api.user.UserManager; import com.atlassian.sal.api.user.UserProfile; @@ -20,8 +21,8 @@ import com.baloise.confluence.digitalsignature.ContextHelper; import com.baloise.confluence.digitalsignature.Markdown; import com.baloise.confluence.digitalsignature.Signature; -import lombok.RequiredArgsConstructor; import org.apache.velocity.tools.generic.DateTool; +import org.jsoup.helper.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +41,6 @@ import static com.atlassian.confluence.renderer.radeox.macros.MacroUtils.defaultVelocityContext; import static com.atlassian.confluence.security.ContentPermission.VIEW_PERMISSION; import static com.atlassian.confluence.security.ContentPermission.createUserPermission; -import static com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext.GLOBAL_CONTEXT; import static com.atlassian.confluence.util.velocity.VelocityUtils.getRenderedTemplate; import static com.baloise.confluence.digitalsignature.api.DigitalSignatureComponent.PLUGIN_KEY; import static java.lang.String.format; @@ -53,9 +53,8 @@ @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Scanned -@RequiredArgsConstructor -public class DigitalSigatureService { - private static final Logger log = LoggerFactory.getLogger(DigitalSigatureService.class); +public class DigitalSignatureService { + private static final Logger log = LoggerFactory.getLogger(DigitalSignatureService.class); private final BandanaManager bandanaManager; private final SettingsManager settingsManager; private final UserManager userManager; @@ -66,12 +65,36 @@ public class DigitalSigatureService { private final ContextHelper contextHelper = new ContextHelper(); private final transient Markdown markdown = new Markdown(); + public DigitalSignatureService( + @ComponentImport BandanaManager bandanaManager, + @ComponentImport SettingsManager settingsManager, + @ComponentImport UserManager userManager, + @ComponentImport LocalNotificationService notificationService, + @ComponentImport MailServerManager mailServerManager, + @ComponentImport PageManager pageManager, + @ComponentImport I18nResolver i18nResolver) { + this.bandanaManager = bandanaManager; + this.settingsManager = settingsManager; + this.userManager = userManager; + this.notificationService = notificationService; + this.mailServerManager = mailServerManager; + this.pageManager = pageManager; + this.i18nResolver = i18nResolver; + } + @GET @Path("sign") - public Response sign(@QueryParam("key") final String key, @Context UriInfo uriInfo) { + public Response sign(@QueryParam("key") final String key, + @Context UriInfo uriInfo) { ConfluenceUser confluenceUser = AuthenticatedUserThreadLocal.get(); String userName = confluenceUser.getName(); - Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); + Signature signature = Signature.fromBandana(bandanaManager, key); + + if (signature == null || StringUtil.isBlank(userName)) { + log.error("Both, a signature and a user name are required to call this method.", + new NullPointerException(signature == null ? "signature" : "userName")); + return Response.noContent().build(); + } if (!signature.sign(userName)) { status(Response.Status.BAD_REQUEST) @@ -79,8 +102,8 @@ public Response sign(@QueryParam("key") final String key, @Context UriInfo uriIn .type(MediaType.TEXT_PLAIN) .build(); } - bandanaManager.setValue(GLOBAL_CONTEXT, key, signature); + Signature.toBandana(bandanaManager, key, signature); String baseUrl = settingsManager.getGlobalSettings().getBaseUrl(); for (String notifiedUser : signature.getNotify()) { notify(notifiedUser, confluenceUser, signature, baseUrl); @@ -91,6 +114,7 @@ public Response sign(@QueryParam("key") final String key, @Context UriInfo uriIn protectedPage.addPermission(createUserPermission(VIEW_PERMISSION, confluenceUser)); pageManager.saveContentEntity(protectedPage, null); } + URI pageUri = create(settingsManager.getGlobalSettings().getBaseUrl() + "/pages/viewpage.action?pageId=" + signature.getPageId()); return temporaryRedirect(pageUri).build(); } @@ -99,27 +123,30 @@ private void notify(final String notifiedUser, ConfluenceUser signedUser, final try { UserProfile notifiedUserProfile = contextHelper.getProfileNotNull(userManager, notifiedUser); - String user = format("%s", - baseUrl + "/display/~" + signedUser.getName(), + String user = format("%s", + baseUrl, + signedUser.getName(), signedUser.getFullName() ); - String document = format("%s", - baseUrl + "/pages/viewpage.action?pageId=" + signature.getPageId(), + String document = format("%s", + baseUrl, + signature.getPageId(), signature.getTitle() ); String html = i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.message.hasSignedShort", user, document); if (signature.isMaxSignaturesReached()) { - html = html + "
      " + i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.warning.maxSignaturesReached", signature.getMaxSignatures()); + html += "
      " + i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.warning.maxSignaturesReached", signature.getMaxSignatures()); } String titleText = i18nResolver.getText("com.baloise.confluence.digital-signature.signature.service.message.hasSignedShort", signedUser.getFullName(), signature.getTitle()); - notificationService.createOrUpdate(notifiedUser, new NotificationBuilder() - .application(PLUGIN_KEY) // a unique key that identifies your plugin - .title(titleText) - .itemTitle(titleText) - .description(html) - .groupingId(PLUGIN_KEY + "-signature") // a key to aggregate notifications - .createNotification()).get(); + notificationService.createOrUpdate(notifiedUser, + new NotificationBuilder() + .application(PLUGIN_KEY) // a unique key that identifies your plugin + .title(titleText) + .itemTitle(titleText) + .description(html) + .groupingId(PLUGIN_KEY + "-signature") // a key to aggregate notifications + .createNotification()).get(); SMTPMailServer mailServer = mailServerManager.getDefaultSMTPMailServer(); @@ -145,7 +172,12 @@ private void notify(final String notifiedUser, ConfluenceUser signedUser, final @Produces("text/html; charset=UTF-8") @HtmlSafe public String export(@QueryParam("key") final String key) { - Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); + Signature signature = Signature.fromBandana(bandanaManager, key); + + if (signature == null) { + log.error("A signature is required to call this method.", new NullPointerException("signature")); + return "ERROR: A signature is required to call this method."; + } Map signed = contextHelper.getProfiles(userManager, signature.getSignatures().keySet()); Map missing = contextHelper.getProfiles(userManager, signature.getMissingSignatures()); @@ -165,9 +197,20 @@ public String export(@QueryParam("key") final String key) { @GET @Path("emails") @Produces("text/html; charset=UTF-8") - public Response emails(@QueryParam("key") final String key, @QueryParam("signed") final boolean signed, @QueryParam("emailOnly") final boolean emailOnly, @Context UriInfo uriInfo) { - Signature signature = (Signature) bandanaManager.getValue(GLOBAL_CONTEXT, key); - Map profiles = contextHelper.getProfiles(userManager, signed ? signature.getSignatures().keySet() : signature.getMissingSignatures()); + public Response emails(@QueryParam("key") final String key, + @QueryParam("signed") final boolean signed, + @QueryParam("emailOnly") final boolean emailOnly, + @Context UriInfo uriInfo) { + Signature signature = Signature.fromBandana(bandanaManager, key); + + if (signature == null) { + log.error("A signature is required to call this method.", new NullPointerException("signature")); + return Response.noContent().build(); + } + + Map profiles = contextHelper.getProfiles(userManager, signed + ? signature.getSignatures().keySet() + : signature.getMissingSignatures()); Map context = defaultVelocityContext(); context.put("signature", signature); diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java index d85a89a..e7d49e4 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java @@ -1,52 +1,84 @@ package com.baloise.confluence.digitalsignature; +import com.atlassian.bandana.BandanaManager; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class SignatureTest { - @Test - void serialize_empty() { - Signature signature = new Signature(); + @Nested + class SerializationTest { + @Test + void serialize_empty() { + Signature signature = new Signature(); - String json = signature.serialize(); + String json = signature.serialize(); - assertEquals("{\"key\":\"\",\"hash\":\"\",\"pageId\":0,\"title\":\"\",\"body\":\"\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[],\"notify\":[]}", json); - } + assertEquals("{\"key\":\"\",\"hash\":\"\",\"pageId\":0,\"title\":\"\",\"body\":\"\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[],\"notify\":[]}", json); + } - @Test - void serialize_initializedObject() { - Signature signature = new Signature(42L, "body text", "title text"); - signature.sign("max.mustermann"); - signature.setMissingSignatures(Set.of("max.muster")); - signature.setNotify(Set.of("max.meier")); + @Test + void serialize_initializedObject() { + Signature signature = new Signature(42L, "body text", "title text"); + signature.sign("max.mustermann"); + signature.setMissingSignatures(Set.of("max.muster")); + signature.setNotify(Set.of("max.meier")); - String json = signature.serialize(); + String json = signature.serialize(); - assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); - } + assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); + } + + @Test + void deserialize_empty() { + assertNull(Signature.deserialize(null)); + assertNull(Signature.deserialize("")); + } + + @Test + void serializeAndDeserialize() { + Signature signature = new Signature(42L, "body text", "title text"); + signature.sign("max.mustermann"); + signature.setMissingSignatures(Set.of("max.muster")); + signature.setNotify(Set.of("max.meier")); + + String json = signature.serialize(); - @Test - void deserialize_empty() { - assertNull(Signature.deserialize(null)); - assertNull(Signature.deserialize("")); + Signature restoredSignature = Signature.deserialize(json); + + assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); + assertEquals(signature, restoredSignature); + } } - @Test - void serializeAndDeserialize() { - Signature signature = new Signature(42L, "body text", "title text"); - signature.sign("max.mustermann"); - signature.setMissingSignatures(Set.of("max.muster")); - signature.setNotify(Set.of("max.meier")); + @Nested + class BandanaWrapperTest { + private final Signature signature = new Signature(1, "test", "title"); + private final BandanaManager bandana = mock(BandanaManager.class); + + @Test + void fromBandana_signature_signature() { + when(bandana.getValue(any(), any())).thenReturn(signature.serialize()); + + Signature readSignature = Signature.fromBandana(bandana, null); + + assertEquals(signature, readSignature); + } - String json = signature.serialize(); + @Test + void fromBandana_string_signatur() { + when(bandana.getValue(any(), any())).thenReturn(signature); - Signature restoredSignature = Signature.deserialize(json); + Signature readSignature = Signature.fromBandana(bandana, null); - assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); - assertEquals(signature, restoredSignature); + assertEquals(signature, readSignature); + } } } From ab6f47992e5621f84b20d1377592131614e71dc3 Mon Sep 17 00:00:00 2001 From: tiliavir Date: Tue, 15 Feb 2022 11:22:56 +0100 Subject: [PATCH 07/16] #82: explanation in Readme --- README.MD | 4 +++- docs/img/classcastexception.png | Bin 0 -> 41811 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/img/classcastexception.png diff --git a/README.MD b/README.MD index a6b9e45..9f3b0ca 100644 --- a/README.MD +++ b/README.MD @@ -13,10 +13,12 @@ Allows confluence users to write contracts in a confluence macro which can be si - receive notifications, when your contract was signed ## ClassCastException issue +![ClassCastException in Macro](./docs/img/classcastexception.png) + If you observe issues in the Macro resulting in a `ClassCastException` please update digital-signature to version 7.0.5, clear the plugin cache (one last time) and restart confluence. -For backroung information please refer to [#82](https://github.com/baloise/digital-signature/issues/82.) +For background information please refer to [#82](https://github.com/baloise/digital-signature/issues/82.) ## Privacy Policy - We do not transfer or store any data outside your Atlassian product. diff --git a/docs/img/classcastexception.png b/docs/img/classcastexception.png new file mode 100644 index 0000000000000000000000000000000000000000..d23dad2ed6ff1d2bdd3348c4020c4ad2c30d5ad6 GIT binary patch literal 41811 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

      A~2i>$vr+M4Y%8JsinMb{DOx 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 y6+C>X((olH`~8H%zXKAKHK_Ub_ Date: Mon, 25 Apr 2022 14:12:53 +0200 Subject: [PATCH 08/16] Updated documentation --- README.MD | 3 ++- development.md | 42 ------------------------------------------ docs/docker.md | 5 +++-- 3 files changed, 5 insertions(+), 45 deletions(-) delete mode 100644 development.md diff --git a/README.MD b/README.MD index 9f3b0ca..38dc82f 100644 --- a/README.MD +++ b/README.MD @@ -18,7 +18,8 @@ Allows confluence users to write contracts in a confluence macro which can be si If you observe issues in the Macro resulting in a `ClassCastException` please update digital-signature to version 7.0.5, clear the plugin cache (one last time) and restart confluence. -For background information please refer to [#82](https://github.com/baloise/digital-signature/issues/82.) +For background information please refer to [#82](https://github.com/baloise/digital-signature/issues/82) +and ['How to clear Confluence plugins cache'](https://confluence.atlassian.com/confkb/how-to-clear-confluence-plugins-cache-297664846.html). ## Privacy Policy - We do not transfer or store any data outside your Atlassian product. diff --git a/development.md b/development.md deleted file mode 100644 index 3820c4b..0000000 --- a/development.md +++ /dev/null @@ -1,42 +0,0 @@ -# Install -https://www.atlassian.com/software/confluence/download-archives - -- Apache Commons FileUpload Bundle -http://central.maven.org/maven2/commons-fileupload/commons-fileupload/1.3/commons-fileupload-1.3.jar -- Atlassian PDK Install Plugin -http://maven-us.nuxeo.org/nexus/content/repositories/public/com/atlassian/pdkinstall/pdkinstall-plugin/0.6/pdkinstall-plugin-0.6.jar - -## License -https://my.atlassian.com/products/index - -# Run -```shell script -set CATALINA_HOME=C:\Users\Public\dev\atlas\Confluence -set JPDA_ADDRESS=4444 -set JPDA_TRANSPORT=dt_socket -%CATALINA_HOME%\bin\catalina.bat jpda start -``` - -http://127.0.0.1:8090/ -```shell script -atlas-install-plugin -p 8090 --context-path / --plugin-key com.baloise.confluence.digital-signature -``` - -or uncomment the atlassian-pdk configuration in pom.xml and use -mvn package confluence:install - -------------------- - -Pure Maven setup (not for the faint of heart, startup is slow on my box) -you will be able to remote debug on port 5005 - -```shell script -mvn confluence:debug -Dproduct.version=7.4.0 -``` - -To redeploy use -```shell script -mvn package confluence:install -Dproduct.version=7.4.0 -``` - -As smtp server use `https://mailtrap.io/` diff --git a/docs/docker.md b/docs/docker.md index 82a33ae..a788f63 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,12 +1,13 @@ Setup following a [tutorial from coffeetime.solutions]( http://coffeetime.solutions/run-atlassian-jira-and-confluence-with-postgresql-on-docker/#Overview_of_series_How_to_run_Jira_and_Confluence_behind_NGINX_reverse_proxy_on_Docker): ```bash -docker run --name=confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:latest docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres +docker run --name=confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:latest ``` Start confluence setup and configure Postgres: -- jdbc:postgresql://192.168.65.2:5432/postgres (`docker inspect postgres` to get ip address) +- jdbc:postgresql://192.168.65.2:5432/postgres (`docker inspect postgres | grep IP` to get ip address) + - in case of IP change search and replace in `/var/atlassian/application-data/confluence.cfg.xml` - user: postgres - password: mysecretpassword (defined above) From 44b4ea443407faefbb9deeea8a8ad4d43c1570ff Mon Sep 17 00:00:00 2001 From: tiliavir Date: Mon, 6 Jun 2022 10:10:18 +0200 Subject: [PATCH 09/16] #82: Tests, error handling of Signature's Bandana access and minor cleanup --- .../DigitalSignatureMacro.java | 8 ++- .../digitalsignature/Signature.java | 17 +++--- .../rest/DigitalSignatureService.java | 15 +++-- .../SignatureSerialisationTest.java | 31 +++++++--- .../digitalsignature/SignatureTest.java | 57 ++++++++++++++----- 5 files changed, 85 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java index aaeb4a8..8f28d40 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java @@ -29,12 +29,12 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.InvalidParameterException; import java.util.*; import static com.atlassian.confluence.renderer.radeox.macros.MacroUtils.defaultVelocityContext; import static com.atlassian.confluence.security.ContentPermission.*; -import static com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext.GLOBAL_CONTEXT; import static com.atlassian.confluence.util.velocity.VelocityUtils.getRenderedTemplate; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; @@ -296,7 +296,9 @@ private Signature sync(Signature signature, Set signers) { save = true; } - if (save) save(loaded); + if (save) { + save(loaded); + } } else { signature.setMissingSignatures(signers); save(signature); @@ -346,7 +348,7 @@ protected String getMailto(Collection profiles, String subject, boo public String urlEncode(String string) { try { - return URLEncoder.encode(string, "UTF-8"); + return URLEncoder.encode(string, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index 20448f0..beb224b 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -41,12 +41,8 @@ public Signature(long pageId, String body, String title) { public static boolean isPetitionMode(Set userGroups) { return userGroups != null - && userGroups.size() == 1 - && userGroups.iterator().next().trim().equals("*"); - } - - String serialize() { - return GSON.toJson(this, Signature.class); + && userGroups.size() == 1 + && userGroups.iterator().next().trim().equals("*"); } static Signature deserialize(String serialization) { @@ -57,7 +53,7 @@ public static Signature fromBandana(BandanaManager mgr, String key) { Object value = mgr.getValue(GLOBAL_CONTEXT, key); if (value == null) { - return null; + throw new IllegalArgumentException("Value is null in Bandana???"); } if (value instanceof Signature) { @@ -76,8 +72,7 @@ public static Signature fromBandana(BandanaManager mgr, String key) { } } - log.error("Could not deserialize {} value from Bandana", value.getClass().getName()); - return null; + throw new IllegalArgumentException(String.format("Could not deserialize %s value from Bandana. Please clear the plugin-cache and reboot confluence. (https://github.com/baloise/digital-signature/issues/82)", value)); } public static void toBandana(BandanaManager mgr, String key, Signature sig) { @@ -88,6 +83,10 @@ public static void toBandana(BandanaManager mgr, Signature sig) { toBandana(mgr, sig.getKey(), sig); } + String serialize() { + return GSON.toJson(this, Signature.class); + } + public String getHash() { if (hash == null) { hash = getKey().replace("signature.", ""); diff --git a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java index 251458f..587b492 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java @@ -65,14 +65,13 @@ public class DigitalSignatureService { private final ContextHelper contextHelper = new ContextHelper(); private final transient Markdown markdown = new Markdown(); - public DigitalSignatureService( - @ComponentImport BandanaManager bandanaManager, - @ComponentImport SettingsManager settingsManager, - @ComponentImport UserManager userManager, - @ComponentImport LocalNotificationService notificationService, - @ComponentImport MailServerManager mailServerManager, - @ComponentImport PageManager pageManager, - @ComponentImport I18nResolver i18nResolver) { + public DigitalSignatureService(@ComponentImport BandanaManager bandanaManager, + @ComponentImport SettingsManager settingsManager, + @ComponentImport UserManager userManager, + @ComponentImport LocalNotificationService notificationService, + @ComponentImport MailServerManager mailServerManager, + @ComponentImport PageManager pageManager, + @ComponentImport I18nResolver i18nResolver) { this.bandanaManager = bandanaManager; this.settingsManager = settingsManager; this.userManager = userManager; diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java index 80efb2a..182d604 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java @@ -1,18 +1,20 @@ package com.baloise.confluence.digitalsignature; - import org.junit.jupiter.api.Test; -import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Date; import static org.junit.jupiter.api.Assertions.*; - class SignatureSerialisationTest { + public static final String SIG_JSON = "{\"key\":\"signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"hash\":\"a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"pageId\":123,\"title\":\"title\",\"body\":\"body\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{\"signed1\":\"Jan 1, 1970, 1:00:09 AM\"},\"missingSignatures\":[\"missing1\",\"missing2\"],\"notify\":[\"notify1\"]}"; + @Test void deserialize() throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(getClass().getResourceAsStream("/signature.ser")); @@ -24,7 +26,13 @@ void deserialize() throws IOException, ClassNotFoundException { () -> assertEquals("[missing1, missing2]", signature.getMissingSignatures().toString()), () -> assertEquals(1, signature.getSignatures().size()), () -> assertTrue(signature.getSignatures().containsKey("signed1")), - () -> assertEquals(9999, signature.getSignatures().get("signed1").getTime()) + () -> assertEquals(9999, signature.getSignatures().get("signed1").getTime()), + + // assert we can still read the old gson serialization + () -> assertEquals(signature, Signature.deserialize(SIG_JSON)), + + // assert that deserialization of the serialization results in the original Signature + () -> assertEquals(signature, Signature.deserialize(signature.serialize())) ); } @@ -35,11 +43,18 @@ void serialize() throws IOException, ClassNotFoundException { signature.getMissingSignatures().add("missing1"); signature.getMissingSignatures().add("missing2"); signature.getSignatures().put("signed1", new Date(9999)); - ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("src/test/resources/signature-test.ser")); - out.writeObject(signature); - out.close(); - ObjectInputStream in = new ObjectInputStream(this.getClass().getResourceAsStream("/signature.ser")); + Path path = Paths.get("src/test/resources/signature-test.ser"); + try(ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(path))) { + out.writeObject(signature); + } + + // assert the serialization we just wrote can be deserialized + ObjectInputStream in = new ObjectInputStream(Files.newInputStream(path)); + assertEquals(signature, in.readObject()); + + // assert the historically serialized class can still be deserialized + in = new ObjectInputStream(this.getClass().getResourceAsStream("/signature.ser")); assertEquals(signature, in.readObject()); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java index e7d49e4..a43c8b3 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java @@ -1,16 +1,17 @@ package com.baloise.confluence.digitalsignature; import com.atlassian.bandana.BandanaManager; +import com.atlassian.bandana.DefaultBandanaManager; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; -import java.util.Set; +import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class SignatureTest { @Nested @@ -28,8 +29,8 @@ void serialize_empty() { void serialize_initializedObject() { Signature signature = new Signature(42L, "body text", "title text"); signature.sign("max.mustermann"); - signature.setMissingSignatures(Set.of("max.muster")); - signature.setNotify(Set.of("max.meier")); + signature.setMissingSignatures(Collections.singleton("max.muster")); + signature.setNotify(Collections.singleton("max.meier")); String json = signature.serialize(); @@ -46,8 +47,8 @@ void deserialize_empty() { void serializeAndDeserialize() { Signature signature = new Signature(42L, "body text", "title text"); signature.sign("max.mustermann"); - signature.setMissingSignatures(Set.of("max.muster")); - signature.setNotify(Set.of("max.meier")); + signature.setMissingSignatures(Collections.singleton("max.muster")); + signature.setNotify(Collections.singleton("max.meier")); String json = signature.serialize(); @@ -60,25 +61,51 @@ void serializeAndDeserialize() { @Nested class BandanaWrapperTest { + private final BandanaManager bandana = mock(DefaultBandanaManager.class); private final Signature signature = new Signature(1, "test", "title"); - private final BandanaManager bandana = mock(BandanaManager.class); + + @Test + void toBandanaFromBandana_readAsWritten() { + ArgumentCaptor stringCapator = ArgumentCaptor.forClass(String.class); + ArgumentCaptor objectCapator = ArgumentCaptor.forClass(Object.class); + + String key = signature.getKey(); + assertNull(Signature.fromBandana(bandana, key), "Should not be there yet."); + + doNothing().when(bandana).setValue(any(), stringCapator.capture(), objectCapator.capture()); + when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); + + Signature.toBandana(bandana, signature); + assertEquals(key, stringCapator.getValue()); + assertEquals(signature.serialize(), objectCapator.getValue()); + + when(bandana.getValue(any(), any())).thenCallRealMethod(); + when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature); + assertEquals(signature, Signature.fromBandana(bandana, signature.getKey())); + } @Test void fromBandana_signature_signature() { - when(bandana.getValue(any(), any())).thenReturn(signature.serialize()); + String key = signature.getKey(); + assertNull(Signature.fromBandana(bandana, key), "Should not be there yet."); - Signature readSignature = Signature.fromBandana(bandana, null); + when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); + when(bandana.getValue(any(), any())).thenCallRealMethod(); + when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature); - assertEquals(signature, readSignature); + assertEquals(signature, Signature.fromBandana(bandana, signature.getKey())); } @Test - void fromBandana_string_signatur() { - when(bandana.getValue(any(), any())).thenReturn(signature); + void fromBandana_string_signature() { + String key = signature.getKey(); + assertNull(Signature.fromBandana(bandana, key), "Should not be there yet."); - Signature readSignature = Signature.fromBandana(bandana, null); + when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); + when(bandana.getValue(any(), any())).thenCallRealMethod(); + when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature.serialize()); - assertEquals(signature, readSignature); + assertEquals(signature, Signature.fromBandana(bandana, signature.getKey())); } } } From c786aaa7f7372e0b8d77fe4a747bbf90940b157c Mon Sep 17 00:00:00 2001 From: tiliavir Date: Sun, 21 Aug 2022 14:03:00 +0200 Subject: [PATCH 10/16] #82: make date serialization independent of JRE --- .github/workflows/ci.yml | 3 ++- .github/workflows/release.yml | 3 ++- .../com/baloise/confluence/digitalsignature/Signature.java | 3 ++- .../digitalsignature/SignatureSerialisationTest.java | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c2908b..da00291 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,9 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: + 'distribution': adopt java-version: '11' - name: Build with Maven run: mvn -B package --file pom.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d07aa91..f4c1668 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,9 @@ jobs: uses: actions/checkout@v2 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: + 'distribution': adopt java-version: '11' - name: Build with Maven 🔧 diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index beb224b..4d378d9 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -2,6 +2,7 @@ import com.atlassian.bandana.BandanaManager; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -18,7 +19,7 @@ @Setter @NoArgsConstructor public class Signature implements Serializable { - public static final Gson GSON = new Gson(); + public static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssz").create(); private static final long serialVersionUID = 1L; private String key = ""; private String hash = ""; diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java index 182d604..b33aca5 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; class SignatureSerialisationTest { - public static final String SIG_JSON = "{\"key\":\"signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"hash\":\"a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"pageId\":123,\"title\":\"title\",\"body\":\"body\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{\"signed1\":\"Jan 1, 1970, 1:00:09 AM\"},\"missingSignatures\":[\"missing1\",\"missing2\"],\"notify\":[\"notify1\"]}"; + public static final String SIG_JSON = "{\"key\":\"signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"hash\":\"a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"pageId\":123,\"title\":\"title\",\"body\":\"body\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{\"signed1\":\"1970-01-01T01:00:09CET\"},\"missingSignatures\":[\"missing1\",\"missing2\"],\"notify\":[\"notify1\"]}"; @Test void deserialize() throws IOException, ClassNotFoundException { From 6082c5eaac3d67166f399b834e0ae2ad5104a724 Mon Sep 17 00:00:00 2001 From: tiliavir Date: Sun, 21 Aug 2022 16:02:11 +0200 Subject: [PATCH 11/16] #82: return null if key is not present --- .../com/baloise/confluence/digitalsignature/Signature.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index 4d378d9..6929c16 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -1,6 +1,7 @@ package com.baloise.confluence.digitalsignature; import com.atlassian.bandana.BandanaManager; +import com.google.common.collect.Sets; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import lombok.Getter; @@ -51,6 +52,11 @@ static Signature deserialize(String serialization) { } public static Signature fromBandana(BandanaManager mgr, String key) { + if (mgr.getKeys(GLOBAL_CONTEXT) == null + || !Sets.newHashSet(mgr.getKeys(GLOBAL_CONTEXT)).contains(key)) { + return null; + } + Object value = mgr.getValue(GLOBAL_CONTEXT, key); if (value == null) { From 1747f91b9f8263f6cf98edd64f8e93d50dce8659 Mon Sep 17 00:00:00 2001 From: tiliavir Date: Mon, 22 Aug 2022 10:02:26 +0200 Subject: [PATCH 12/16] #82: docker documentation --- docs/docker.md | 9 +++++++-- docs/img/db.png | Bin 0 -> 46749 bytes 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 docs/img/db.png diff --git a/docs/docker.md b/docs/docker.md index a788f63..6012ac2 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,8 +1,11 @@ Setup following a [tutorial from coffeetime.solutions]( http://coffeetime.solutions/run-atlassian-jira-and-confluence-with-postgresql-on-docker/#Overview_of_series_How_to_run_Jira_and_Confluence_behind_NGINX_reverse_proxy_on_Docker): ```bash -docker run --name postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres -docker run --name=confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:latest +mkdir -p $HOME/docker/volumes/postgres +mkdir -p $HOME/docker/volumes/confluence +docker run --name postgres -v $HOME/docker/volumes/postgres:/var/lib/postgresql/data -e POSTGRES_PASSWORD=mysecretpassword -d postgres +docker run --name=confluence -v $HOME/docker/volumes/confluence:/var/atlassian/application-data/confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:latest +docker inspect postgres | grep IPAddress # get the IP address of the postgres container ``` Start confluence setup and configure Postgres: @@ -11,5 +14,7 @@ Start confluence setup and configure Postgres: - user: postgres - password: mysecretpassword (defined above) +![](img/db.png) + Skip tutorial Create new space "Test" diff --git a/docs/img/db.png b/docs/img/db.png new file mode 100644 index 0000000000000000000000000000000000000000..c0fa49ba9e83921d71c2e9d2a2d54d14b2de7430 GIT binary patch literal 46749 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%0A=P+aVg9weID{iu8>6{D2>mZYl`nw#jA#^ zeb41vJ^0=d>B^wVapvVG&=VvO$BnF7mLrPMwo#e`qX=oJg*trySxE9f34JlMM`*ah ze4F5qqWM>1s60S6)5xTwza)TH&Mn42tnM%Xk!+@Nm{{bY6BH-SR`hmOT zSs2Tyx!3;67bE(qb$aS6B<`u&6dCNM?l~1@*KqM$O0#Y*CJE@|w}M>6E=mjsxBR!j z=@y*@ygk9bGTz&B;=_>Zz(;*l_ttnNOyvL0spP8iq zYjnfDkIKKVH~&uD+;`?e!rJg-DO~TEtd+?ftlfKIjUD2OA$9weS54;!MM(?ohC3LE z`0GpiZg|zr2kED%$eY5}b?W?61Izf&Bhvh~cPN@d8)%er&HZN!tj63l0@mK!QDqOU zx#-;kTne}%N=q=|cC*}}rl1@DFA9$C5_+NwVavu3ziA?KzT6LrGIl`|{TeaEGjHShDTxx4-R1O^aA4^tAv?c|A@$CwryH+dOCLfOFxYlb*$tz4DtS&eCo53 zxxG`|dV0D+i4NDjherF&6~TVoog-)v6iy-Js7FdRnRlM8Nfm82#tF_WU0f`4Qm98_ zT!pU<*bmVY>Ye8r8s<>%suML&t&`oiZ#^g*|8cUGkzL~fd~z)#4;uix%k5t~tgc>D ztzPrSy8ncXzUgAumxFJnJ>{Y^(n^Z*p1ek2varBuoD%|H7+EjpM>b0bo5|8!W`G+J zzv3V1H;&JUr?PReHy71|w03o5Nvk$5>qxcQ{qlE~9+`RR7Ht!C(cQx=8y+VZh;`Bv zb#Swj=^CO+wLF~;)2cd&0Jn6wZKLg0&M(%+8%yq3=H;_(Wy7uxyN?bePlhOOyr0}a zC^Xr3z7)Kx>b#!lJL74)p0UGLrhYH|TyN_T^*cffd2^k4*a{228qtptCkto2UUV#G z-yed8Twg~W05H1Il?Qu$@@imw-1VzJ6M=(p&su|e5#u0u@*O0)mPB6@NaUO*%MkbU zZ#Ztu3Uit&5o@X`^=u2Vu7CPu6&7;3J#!CRVh`Kj`R~w=E-ud-`R$E*Rmb^aN|D*s zFL2YsJ}QZ%-Gv5sed4#`__yx1Mi`^0>HI55=D$|t{%_UmWZyoYQO9k+(|Lh6PJ-#p z+Z)k4tIW5Q?g;({zdu59L&qh5b*le`9#uBN{@`1V!kwj41kM{as{dMd{NF;37awnF z1Btv(1y%Pg8V@9}{vnCNe6kF`Z~0p+sR8w+!?5Ik37UIK2rF_O*ZreR9=^pl-u;GdP-#%J~*>1(|4z@%A88nq-jV!T8ts|S*zbMIw%rqO3JoSn}7%F^h z(7SY1knaV%7lU9`7JGf=&&F88DpGgL9bO>*lvf1 zzbO7q7vMa)=pCQme-r=2O_eUy3ht*my9o7;Y;9>&T{>VFUW7sbfN?_ptsi4-Z zo)1gX6D;e?{N5U6lAs8B1u&bk(^XS^=2a&Iqa_ZI*IiGWVUg=My-6jgFhcwSBT%RM zN-Ulk5y2%e^Y0s%SEnKG-t9X(&}}KB&D0BjA=!3$wO=+J6QL#p*dxX<%Cz75bR}7< zmkrJZMa*mWEE9{V2;@(qxPuxErt`Wq7WPCElwXWiBdqbm2gTYd34y7PVkxQSfMNKi zR)I~L&SflfDJ*Vy&RNeMmmcUoi*GJ>M>DpVvUE3T1H4`cDzKoLa=HL$NzlVD7@qIr z;zE5%OfBxulOGp+gbPS$=0@e~>%$v~Gl*|a;MsyxQ`7M!(a~hJR#)IA>{r^sRqo9P zXAwVV-k6M%tDArXKU6kl2a;w2Fs*z!@%Zm{PGM4q`=l06e}l0w$El0}FS;cb*Tg@e z@&kLhVI<{9X109JdE%R_>5H1L!7|C80`eG4cHhYz{^9AwEX%?NjX%!$HwB#PE^lip zI@9H5YJv-MpDMTQYfn^3O6bI~xM8~*k~&UFe@<*+pE&tZ=ul9R$49=vANY>!xXA&2 zRYo6_mb^b@u2W&ZL3F5P_es@CQob!{tF+5-MhC)E+Xko***&f;7em&!jX{j*Vr@y`$84f`)yBHllGDmnj z$?rNzHJ=~uM2B;X%68P#j4~c$lB55Q>P)&EA7BrNG)-t;;^>#5^z&5yHcC@K6T zWafAHP@b;zY}67|{HvwfiV9eiu5?CPdw2W8SVb#NOJJD0EAe)akM;3#u%!arr$KR@ z#6Z9~XM>Kx4(#p^rKX1Ye$`lC)d!_rB_skT#vkIMf3F}IO=kDM0)g0)@cvVbrQ7P% z&n@kLkBRe@Cc(Fj=?BNomxmk2EYIlLTuZz)o{UePPxWD!nhj%dQ*JNndN5Oi|GLJv(a* zEq!QAoJLX3dF$IxDU}(H5BbN zBr%%7h|rg+5r1`zvy%-y_K{N`HU)!I3X#1MjTCABn3N?lQ(`C8RYZXKVR(2rOAkKT zD{$1mJp^z+)aMygqz=;>Vp0S43SjL&iKPGR(K&}> zWTZXsgta5=7S=UBrr!$8H~>uzZB1MUtn&}?<3mIRTQ`Qp6@@mO>9u)X>(S z-oJwt#Eu!uf+Ac;MC4*H)*GTQ%rGC5NvoP_wi03e%ESCNE{CbOgwyft7M3I#i4kf- z`>v3;{GKFM7iM5Nr2-;97giQ8hZM!Wbv+0QsZd@D!46U&JG^tq$(JBTe-$Fr4Ey_g0Eu=iQuK+$odIjl1<#;)a@)|&3@%E zYHyOdD9W7Z!lLeX33}i*AwC-O`%*vQu7teYJ8^>~enQOJiFdO^MjAJ5}cm z<~n%A#Twu~`nI*J>9p$Jz!Ym9tcj5<_k~QH&sRhU(1PR}XO+vUuI~`wZNm6x>)%aY zt{`ke`lW;0>WhB}?MO&er4dWnZR+YbwF6f{0eFBE%o|Mdjn1)&`V`Z`TrdHdVqG%a zEnsuip-9zb9gw?ZMX#L+q?2ZZEUBf?W^@7BdAEvs`$VWSAO;9xlgpPYGw1Dh@bJ*C zm}Ybip5CkujCXe~@nMXUDj@P11y#Kmj_iI1t(fmDOmg2pzT%)7b6o&OmOd|?yU#QQ zosak2WX;qx%!N!tg+O)?d+rqujCz`37s#V};U)&bmjKCMzhFi`AnlJ=e*i3tzFg9c0y%+ zndr7xd|pBUE7YOACuQV0f1c2nCLCuy`ZaF}kjGK=kyh2!^TR9Cm^N)moLR&IufVa7 zh_1m53n(FH^V1WBW>%izxdomH&*k-Ox3Z1)m3JDg>Qj80l?kY*< zS{|;=tXl6k$WaFT{vyz2Kc=40;0DIPWdrJ^bZRI_(%>SrtrxwJ8~`yGb8c+;*>)mX zge~KdZU*XtEKAvYNZkxN7#qa+J-6&XEyFntuKK2FXnoP{pn~l_L)pG+{=lT7T}gfv zZk+v|c(nUtXeFz;?d|k&ftXce*2?1dw00G$I>!b3i4%UNeTn5e9sa@A z%}H;2QJuh(-o6G1wRMha*7(z%t?ZLo16Tr6idlcrSG{yD?mEP*D zm#>xxFL>fagcr@CwnzbG z#|;Z$zdqI(?0a?k&ccAc1L8C3m192tib8r0#&$+zD!C8kz$a2d%_r0zuX_USILb`L zEv}zaO1?4}_PF21yC8SbG1VAjPz4`tKiAm5cO=S{L_Z>HW!zgt6D)6)&t{|FMEz-0 zZ!HKlw8?;>_wk}Cu?ll>|&#(ag^1iOV2S_xm1WyP@JBe`iK)>z< znL3tPIA@|?BB5-Af2_LOU^;Zajpp@UaUHK@O|uy1etAO9(P?OeX*(?+op^ngCCMvC zys6Ef0Ho2H{VrOaBtIeUbc(n~Q5bJ*QZRfShBGSEci?v5$g zBDF~KE24M3(8ER_&X`PR6dD)ZW37ue^uMqUECOqd_k3GZW(5N@Th)*DZ+YF73RwMC z6Vkr*EZ|eK1Xa+^r>F6~W4+e?c!24ErSEXgo_NnD#}Drx87~3&SrNncO+LHYkR;~n zn@+i;nh(0v+W_!GUuHsfvr6bq&-O!EEo+$GHH=Rtshc(A-hV3KA2TE5YxO`R zq|#sT`pCcG z%)e<>rT%#(uK$Z&u3*Z3xvHaJm(Fo~G4rKfUZsY)pQWf@<{ir_w`HS3X}7~OUUDvJ zPx{XYGhKD)3pWiiNHJ3QFz-~CJLjWV$bF}Hu7Wd$IMI1&c)0?GyI5aHJLcV&4A3&+95c3u2GLK zFPE*qnMNer_rG@CXn7?#&UPLrp<16*GQs0OZL|;1S@X;i^l`_s*_NVV*$7)R^1wri z19zne%w4F+|CC(+4DE~qD5Ax4NpyFcYp0=brfiC)QzB81b`l@XKk*JyLtd_{9XJrw zcs?Vj+DUFxbE&0*_H>r3UtE0Ln{UBLRhebk!@_uEKxfx#J!LTM<8a_yurr$E`huUi9^Pa^>J8cKRiYP< z1dRz9TJMb(#Uj{OATPng+(BMhJr=a&DrT?zwwm{xp}QdB9-a?5EBvT$L#ARG6zCkp zSzn|%CZB#xp8RU&DNVlPmoikrkI!EbtR4rwN)nP6WbJJoJ@&E23p5Tg0xD@!j)hO; zojzpQC=7vWbT&-N(&$LTTbBqFRLx`rcEvGEmrt1Us$&EoajE^$UR&O<$CW(8mny~M z{H~yc5}U;XejL-R@kD~u^FTbn^LW~4zBT3&sKor(;^4YtE7nf&E1f^kO#YFRhs@a_ zQ;^{7JdYu;h@+Ba$64?(2?~Jr7VbHje;~#~+*ydntZp=@3SUu3s66FF4~bdNX*~j~ z4G3SYoR2Iw+=A|cE#$`4UajtL5*Rl^^SarRLqX#0b#7^uqpOFbPaF@ai`osr87 zC{cK-`-Pjc@chFN1j!czc=N?NASY{F9V(z!HBP8>^$3rfW>5|bsOpmtVaWOAgl(E4uO`RNg%&a`Oh zs+M83bRYBfH~9Ro0xp5Q81q^p|2`e5!vvXyo(;B_q;L~-O& zK-8-#6Bkka8K#Cz2g-01G2%#AWw#t5gAOUX>>pyqZI{o7@j%_SP&%s4(6Z=)Itmo* z%F2Tz4v#a|aIAJPfGJVf)T}H$maKu%Bb8jT(|Qzb8**~)On^7)HoeXw7!2FUmWKh) zoqzff%@9gvb}oSJF(%I&b=!2aIJ3&LGnM_^w5{dld7eD)t8yV#m%5LnH={A8nT868 zN3fY-kAE0=tsDL5sNL#$Op5PUZ-sHj!9|64kx^`SPQ3PJcZZ7+sjSC_;h`Wui!}6* zIV+pjt%T1JC3v*2l;JoFr@PCS_;lK&0hR#@Kh+x1jh-{4nfiuPI=is*^UpcjxxgGts~Ph89^@G4W_+9>X}}6uDG6>rsN(4U&~Gs>gkT#l1)xtntN}_ zN6?h7Lri?1R;XWHbW@&);z9atpJI;kdhsw_TAl}n^=ZPW3GVPvx^fZOm5q)kj~zZ- zYc6-M6I88&0s%LR_fnbj%=^{U>^B#R@c>y{R$^1EM}zPelWJ3AqD2M%6{_6aa{w3H zkE8+7jrct&#EuA_j<4CI;?Tv@N`YeqbFzL`W4`mldYZx)zkFHK}?mUz`0FuBqI_4F@!)QKIqf;JXzHks9JGwcEFXv-A8!vk9<=wQj zQbYg4uvKU@bZM9+ZKKH-Y`%OyYuFX>WEWj1GMTG!OJS!_i1sNWH(5K z``4WCS59~%JzsNJz_69~A2-!XeaOYZX~`iRDt~`v=O{ze7-8(HH_@siB=S5SA?)O@ z*s*>aAZN0}GI41)#M2`hxWd|=wZO`D{JKZ_^kvMNYEQSV)N}naew4I8jStg^Z5&&D zZ!ZsSt~GV-^K{nXU3_fJj-B$hvGhW}e+oIqWX`GE(|qZXWU%k$UtuevRUiJ{Fm>rU zxsUbNW3`T~&{V&`>F}fA#$s3HgM{OqJyX|<4gnXa{UzRhKPo4McSS8j<@7k7x>USf z`(z7u1(!Q-&}v}felUB7otmos+Ff^5buog_h)MGcoF%EUR=#pP5T9Qf-r zL%To)zyL>S!=NAs-MA4aB7ij0#&{ zXZgZt)i+pcmbTGC?r#@&e4wG$h3opZcFSU!9&tg66-{gX7Q%$Fxd$`(<=x2S_54Hy zG_8Ds*v@hmQ&gP?egXg8z1pe;L(io}>0v&khnmexkLZ{L+Dk$yveQ#@c$3IXB~OiwchRWYzFJC!X&Z@d-$SWe|a^n4ZMK;{Gf+*qGK3G3F# zum}-6cl`mfdF1iDsIWXSH*tAh_$lI*q=fqD(7KM9>(|dByix~m-F8*ci~vYl){kGi ztr8&B04+6Q{~nWR?Nr(%-e%}{x3W~zW*{kacdok;8O3oGd4c2WFA8aSEYum;V)MbP5$`hYR(iQf zERxk|dxM)oeD?P8-%STrg{Rv+AvKy=w|wazY^Vf!Q_xUaIM#QHIWru%Lt{Fh5wsfl zbXJzIICNI{5sX&j_devA8rHHwnZ+5H@pbY@i3@aOAe|*J^U$Rvi{+)1|JRKraGK9G z+;Ud0tlmz}VmEMO(CWkyY!LSP&VEO6Hnalm4*7@u zN34Uzg8=|l{RbE2vvtO8v(kY(6KK4XxbLF5IxGsGTs@`@4x0NVpQ00Xhfjqm*bbIE zwu(*~GcH^Ui#P`#?m0(FQB^0bRm=IN!^m<{_~`_CQm#9^$bJhW=>CzB~|bwhi04RULOcvv6zpGqqgh%Ye{n?%e?o} zJS2ACAyMn^H?%*E)1#q!1!IWrprCsFbcmV#=X{Gio50Oi?dvaB=YRCb_~JvshzkLO ze*Ae?+k`U~%C4U)rE8Uyj?^@E)ha^HiGy~IazF%04|tcmj>0voU~e@?AyzXy5Y_9< zMF=4fS_`|Uz-0UZ73!@Sk;*Nc#RvOwMDP14>xXxAbUT=|&Z1&_1Vy7mVO+ULu8)j1 zNLGaJ-B>7)si?s(JOej*lCS#`vDeMH2K_E6mF24T>uQFA?#%QV8(iS@P=9A8 zC7i|q`d($&Jo8L{zj4CDc;qbeO*e$L5tW9xCtzTlY7{0PJAA?| z$At>fk-(>x^VpNG^@rTexm}*5U}u5LOneJZG$ZN>xJdw29W_0@2xag6rG}h968cwK zre-9=nUeiWj)Pu^-@D&_ZT>CkSu;)#m|ZccLP*@G?x3Ge%9de)7M0NU-;Pb85-I=xH@;gkgY;q=V zrvJjpe<;l1!`K58CBz-cxA#63hK^;@h*6^!AOXh0eM)rV8YF-CMB?db@`g)fyA^ZH zgh1CN&E6~F$!8j-h}R@qfWxi%JQzY|ExAK$oseyv9}LbQP?VJXS0$yLg{e)oR>Xc>5W literal 0 HcmV?d00001 From f1338e79d8c7049bf6cbbe280e53afb0a628501d Mon Sep 17 00:00:00 2001 From: Peter Kullmann Date: Fri, 8 Sep 2023 15:42:27 +0200 Subject: [PATCH 13/16] Revert Signature --- .../digitalsignature/Signature.java | 352 ++++++++++-------- .../digitalsignature/Signature2.java | 180 +++++++++ 2 files changed, 370 insertions(+), 162 deletions(-) create mode 100644 src/main/java/com/baloise/confluence/digitalsignature/Signature2.java diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index 6929c16..e084eed 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -1,175 +1,203 @@ package com.baloise.confluence.digitalsignature; -import com.atlassian.bandana.BandanaManager; -import com.google.common.collect.Sets; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; import java.io.Serializable; -import java.util.*; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public class Signature implements Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + private String key = ""; + private String hash = ""; + private long pageId; + private String title = ""; + private String body = ""; + private long maxSignatures = -1; + private long visibilityLimit = -1; + private Map signatures = new HashMap<>(); + private Set missingSignatures = new TreeSet<>(); + private Set notified = new TreeSet<>(); + + public Signature() { + } -import static com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext.GLOBAL_CONTEXT; -import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; + public Signature(long pageId, String body, String title) { + this.pageId = pageId; + this.body = body; + this.title = title == null ? "" : title; + hash = sha256Hex(pageId + ":" + title + ":" + body); + key = "signature." + hash; + } + + public static boolean isPetitionMode(Set userGroups) { + return userGroups != null && userGroups.size() == 1 && userGroups.iterator().next().trim().equals("*"); + } + + public String getHash() { + if (hash == null) { + hash = getKey().replace("signature.", ""); + } + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getProtectedKey() { + return "protected." + getHash(); + } + + public long getPageId() { + return pageId; + } -@Slf4j -@Getter -@Setter -@NoArgsConstructor -public class Signature implements Serializable { - public static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssz").create(); - private static final long serialVersionUID = 1L; - private String key = ""; - private String hash = ""; - private long pageId; - private String title = ""; - private String body = ""; - private long maxSignatures = -1; - private long visibilityLimit = -1; - private Map signatures = new HashMap<>(); - private Set missingSignatures = new TreeSet<>(); - private Set notify = new TreeSet<>(); - - public Signature(long pageId, String body, String title) { - this.pageId = pageId; - this.body = body; - this.title = title == null ? "" : title; - this.hash = sha256Hex(pageId + ":" + title + ":" + body); - this.key = "signature." + hash; - } - - public static boolean isPetitionMode(Set userGroups) { - return userGroups != null - && userGroups.size() == 1 - && userGroups.iterator().next().trim().equals("*"); - } - - static Signature deserialize(String serialization) { - return GSON.fromJson(serialization, Signature.class); - } - - public static Signature fromBandana(BandanaManager mgr, String key) { - if (mgr.getKeys(GLOBAL_CONTEXT) == null - || !Sets.newHashSet(mgr.getKeys(GLOBAL_CONTEXT)).contains(key)) { - return null; - } - - Object value = mgr.getValue(GLOBAL_CONTEXT, key); - - if (value == null) { - throw new IllegalArgumentException("Value is null in Bandana???"); - } - - if (value instanceof Signature) { - // required for downward compatibility - update for next time. - Signature signature = (Signature) value; - toBandana(mgr, key, signature); - return signature; - } - - if (value instanceof String) { - try { - return deserialize((String) value); - } catch (Exception e) { - log.error("Could not deserialize String value from Bandana", e); - return null; - } - } - - throw new IllegalArgumentException(String.format("Could not deserialize %s value from Bandana. Please clear the plugin-cache and reboot confluence. (https://github.com/baloise/digital-signature/issues/82)", value)); - } - - public static void toBandana(BandanaManager mgr, String key, Signature sig) { - mgr.setValue(GLOBAL_CONTEXT, key, sig.serialize()); - } - - public static void toBandana(BandanaManager mgr, Signature sig) { - toBandana(mgr, sig.getKey(), sig); - } - - String serialize() { - return GSON.toJson(this, Signature.class); - } - - public String getHash() { - if (hash == null) { - hash = getKey().replace("signature.", ""); - } - return hash; - } - - public String getProtectedKey() { - return "protected." + getHash(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((key == null) ? 0 : key.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - - return Objects.equals(key, ((Signature) obj).key); - } - - public Signature withNotified(Set notified) { - this.notify = notified; - return this; - } - - public Signature withMaxSignatures(long maxSignatures) { - this.maxSignatures = maxSignatures; - return this; - } - - public Signature withVisibilityLimit(long visibilityLimit) { - this.visibilityLimit = visibilityLimit; - return this; - } + public void setPageId(long pageId) { + this.pageId = pageId; + } - public boolean hasSigned(String userName) { - return signatures.containsKey(userName); - } + public String getBody() { + return body; + } - public boolean isPetitionMode() { - return isPetitionMode(getMissingSignatures()); - } + public void setBody(String body) { + this.body = body; + } - public boolean sign(String userName) { - if (!isMaxSignaturesReached() && !isPetitionMode() && !getMissingSignatures().remove(userName)) { - return false; + public Map getSignatures() { + return signatures; } - getSignatures().put(userName, new Date()); - return true; - } + public void setSignatures(Map signatures) { + this.signatures = signatures; + } + + public Set getMissingSignatures() { + return missingSignatures; + } + + public void setMissingSignatures(Set missingSignatures) { + this.missingSignatures = missingSignatures; + } + + public long getVisibilityLimit() { + return visibilityLimit; + } + + public void setVisibilityLimit(long visibilityLimit) { + this.visibilityLimit = visibilityLimit; + } + + public long getMaxSignatures() { + return maxSignatures; + } + + public void setMaxSignatures(long maxSignatures) { + this.maxSignatures = maxSignatures; + } - public boolean isMaxSignaturesReached() { - return maxSignatures > -1 && maxSignatures <= getSignatures().size(); - } - - public boolean isSignatureMissing(String userName) { - return !isMaxSignaturesReached() && !hasSigned(userName) && isSignatory(userName); - } - - public boolean isSignatory(String userName) { - return isPetitionMode() || getMissingSignatures().contains(userName); - } - - public boolean hasMissingSignatures() { - return !isMaxSignaturesReached() && (isPetitionMode() || !getMissingSignatures().isEmpty()); - } + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Set getNotify() { + return notified; + } + + public void setNotify(Set notify) { + this.notified = notify; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Signature other = (Signature) obj; + if (key == null) { + return other.key == null; + } else return key.equals(other.key); + } + + public Signature withNotified(Set notified) { + this.notified = notified; + return this; + } + + public Signature withMaxSignatures(long maxSignatures) { + this.maxSignatures = maxSignatures; + return this; + } + + public Signature withVisibilityLimit(long visibilityLimit) { + this.visibilityLimit = visibilityLimit; + return this; + } + + public boolean hasSigned(String userName) { + return signatures.containsKey(userName); + } + + public boolean isPetitionMode() { + return isPetitionMode(getMissingSignatures()); + } + + public boolean sign(String userName) { + if (!isMaxSignaturesReached() && !isPetitionMode() && !getMissingSignatures().remove(userName)) { + return false; + } else { + getSignatures().put(userName, new Date()); + return true; + } + } + + public boolean isMaxSignaturesReached() { + return maxSignatures > -1 && maxSignatures <= getSignatures().size(); + } + + public boolean isSignatureMissing(String userName) { + return !isMaxSignaturesReached() && !hasSigned(userName) && isSignatory(userName); + } + + public boolean isSignatory(String userName) { + return isPetitionMode() || getMissingSignatures().contains(userName); + } + + public boolean hasMissingSignatures() { + return !isMaxSignaturesReached() && (isPetitionMode() || !getMissingSignatures().isEmpty()); + } + + @Override + public Signature clone() throws CloneNotSupportedException{ + return (Signature) super.clone(); + } } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java new file mode 100644 index 0000000..e57dfad --- /dev/null +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java @@ -0,0 +1,180 @@ +package com.baloise.confluence.digitalsignature; + +import com.atlassian.bandana.BandanaManager; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import static com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext.GLOBAL_CONTEXT; +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; + +@Slf4j +@Getter +@Setter +@NoArgsConstructor +public class Signature2 implements Serializable { + public static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssz").create(); + private static final long serialVersionUID = 1L; + private String key = ""; + private String hash = ""; + private long pageId; + private String title = ""; + private String body = ""; + private long maxSignatures = -1; + private long visibilityLimit = -1; + private Map signatures = new HashMap<>(); + private Set missingSignatures = new TreeSet<>(); + private Set notify = new TreeSet<>(); + + public Signature2(long pageId, String body, String title) { + this.pageId = pageId; + this.body = body; + this.title = title == null ? "" : title; + this.hash = sha256Hex(pageId + ":" + title + ":" + body); + this.key = "signature." + hash; + } + + public static boolean isPetitionMode(Set userGroups) { + return userGroups != null + && userGroups.size() == 1 + && userGroups.iterator().next().trim().equals("*"); + } + + static Signature2 deserialize(String serialization) { + return GSON.fromJson(serialization, Signature2.class); + } + + public static Signature2 fromBandana(BandanaManager mgr, String key) { + if (mgr.getKeys(GLOBAL_CONTEXT) == null + || !Sets.newHashSet(mgr.getKeys(GLOBAL_CONTEXT)).contains(key)) { + return null; + } + + Object value = mgr.getValue(GLOBAL_CONTEXT, key); + + if (value == null) { + throw new IllegalArgumentException("Value is null in Bandana???"); + } + + if (value instanceof Signature2) { + // required for downward compatibility - update for next time. + Signature2 signature = (Signature2) value; + toBandana(mgr, key, signature); + return signature; + } + + if (value instanceof String) { + try { + return deserialize((String) value); + } catch (Exception e) { + log.error("Could not deserialize String value from Bandana", e); + return null; + } + } + + throw new IllegalArgumentException(String.format("Could not deserialize %s value from Bandana. Please clear the plugin-cache and reboot confluence. (https://github.com/baloise/digital-signature/issues/82)", value)); + } + + public static void toBandana(BandanaManager mgr, String key, Signature2 sig) { + mgr.setValue(GLOBAL_CONTEXT, key, sig.serialize()); + } + + public static void toBandana(BandanaManager mgr, Signature2 sig) { + toBandana(mgr, sig.getKey(), sig); + } + + String serialize() { + return GSON.toJson(this, Signature2.class); + } + + public String getHash() { + if (hash == null) { + hash = getKey().replace("signature.", ""); + } + return hash; + } + + public String getProtectedKey() { + return "protected." + getHash(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + return Objects.equals(key, ((Signature2) obj).key); + } + + public Signature2 withNotified(Set notified) { + this.notify = notified; + return this; + } + + public Signature2 withMaxSignatures(long maxSignatures) { + this.maxSignatures = maxSignatures; + return this; + } + + public Signature2 withVisibilityLimit(long visibilityLimit) { + this.visibilityLimit = visibilityLimit; + return this; + } + + public boolean hasSigned(String userName) { + return signatures.containsKey(userName); + } + + public boolean isPetitionMode() { + return isPetitionMode(getMissingSignatures()); + } + + public boolean sign(String userName) { + if (!isMaxSignaturesReached() && !isPetitionMode() && !getMissingSignatures().remove(userName)) { + return false; + } + + getSignatures().put(userName, new Date()); + return true; + } + + public boolean isMaxSignaturesReached() { + return maxSignatures > -1 && maxSignatures <= getSignatures().size(); + } + + public boolean isSignatureMissing(String userName) { + return !isMaxSignaturesReached() && !hasSigned(userName) && isSignatory(userName); + } + + public boolean isSignatory(String userName) { + return isPetitionMode() || getMissingSignatures().contains(userName); + } + + public boolean hasMissingSignatures() { + return !isMaxSignaturesReached() && (isPetitionMode() || !getMissingSignatures().isEmpty()); + } +} From 52013258db22035b55c43ce3e7a9e6fc156123cf Mon Sep 17 00:00:00 2001 From: Peter Kullmann Date: Fri, 8 Sep 2023 16:47:24 +0200 Subject: [PATCH 14/16] Fix signature class cast issue __WIP__ --- .../digitalsignature/ContextHelper.java | 6 ++-- .../DigitalSignatureMacro.java | 22 +++++++-------- .../digitalsignature/Signature.java | 1 + .../digitalsignature/Signature2.java | 16 ++++++++--- .../rest/DigitalSignatureService.java | 21 ++++++++------ .../DigitalSignatureMacroTest.java | 2 +- .../SignatureSerialisationTest.java | 8 +++--- .../digitalsignature/SignatureTest.java | 28 +++++++++---------- 8 files changed, 58 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java b/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java index ff65eaa..2c67bc6 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/ContextHelper.java @@ -11,7 +11,7 @@ import static java.lang.String.format; public class ContextHelper { - public Object getOrderedSignatures(Signature signature) { + public Object getOrderedSignatures(Signature2 signature) { SortedSet> ret = new TreeSet<>(Comparator.comparing((Function, Date>) Entry::getValue) .thenComparing(Entry::getKey)); ret.addAll(signature.getSignatures().entrySet()); @@ -38,7 +38,7 @@ public final Set union(Set... sets) { public Map getProfiles(UserManager userManager, Set userNames) { Map ret = new HashMap<>(); - if (Signature.isPetitionMode(userNames)) return ret; + if (Signature2.isPetitionMode(userNames)) return ret; for (String userName : userNames) { ret.put(userName, getProfileNotNull(userManager, userName)); } @@ -52,7 +52,7 @@ public UserProfile getProfileNotNull(UserManager userManager, String userName) { public SortedSet getOrderedProfiles(UserManager userManager, Set userNames) { SortedSet ret = new TreeSet<>(new UserProfileByName()); - if (Signature.isPetitionMode(userNames)) return ret; + if (Signature2.isPetitionMode(userNames)) return ret; for (String userName : userNames) { ret.add(getProfileNotNull(userManager, userName)); } diff --git a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java index 8f28d40..f77e527 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacro.java @@ -82,10 +82,10 @@ public String execute(Map params, String body, ConversionContext } Set userGroups = getSet(params, "signerGroups"); - boolean petitionMode = Signature.isPetitionMode(userGroups); + boolean petitionMode = Signature2.isPetitionMode(userGroups); Set signers = petitionMode ? all : contextHelper.union(getSet(params, "signers"), loadUserGroups(userGroups), loadInheritedSigners(InheritSigners.ofValue(params.get("inheritSigners")), conversionContext)); ContentEntityObject entity = conversionContext.getEntity(); - Signature signature = sync(new Signature(entity.getLatestVersionId(), body, params.get("title")).withNotified(getSet(params, "notified")).withMaxSignatures(getLong(params, "maxSignatures", -1)).withVisibilityLimit(getLong(params, "visibilityLimit", -1)), signers); + Signature2 signature = sync(new Signature2(entity.getLatestVersionId(), body, params.get("title")).withNotified(getSet(params, "notified")).withMaxSignatures(getLong(params, "maxSignatures", -1)).withVisibilityLimit(getLong(params, "visibilityLimit", -1)), signers); boolean protectedContent = getBoolean(params, "protectedContent", false); if (protectedContent && isPage(conversionContext)) { @@ -100,7 +100,7 @@ public String execute(Map params, String body, ConversionContext } @NotNull - private Map buildContext(Map params, ConversionContext conversionContext, ContentEntityObject page, Signature signature, boolean protectedContent) { + private Map buildContext(Map params, ConversionContext conversionContext, ContentEntityObject page, Signature2 signature, boolean protectedContent) { ConfluenceUser currentUser = AuthenticatedUserThreadLocal.get(); String currentUserName = currentUser.getName(); boolean protectedContentAccess = protectedContent && (permissionManager.hasPermission(currentUser, Permission.EDIT, page) || signature.hasSigned(currentUserName)); @@ -135,7 +135,7 @@ private Map buildContext(Map params, ConversionC return context; } - private void ensureProtectedPage(ConversionContext conversionContext, Page page, Signature signature) { + private void ensureProtectedPage(ConversionContext conversionContext, Page page, Signature2 signature) { Page protectedPage = pageManager.getPage(conversionContext.getSpaceKey(), signature.getProtectedKey()); if (protectedPage == null) { ContentPermissionSet editors = page.getContentPermissionSet(EDIT_PERMISSION); @@ -160,7 +160,7 @@ private void ensureProtectedPage(ConversionContext conversionContext, Page page, } } - private boolean hideSignatures(Map params, Signature signature, String currentUserName) { + private boolean hideSignatures(Map params, Signature2 signature, String currentUserName) { boolean pendingVisible = isVisible(signature, currentUserName, params.get("pendingVisible")); boolean signaturesVisible = isVisible(signature, currentUserName, params.get("signaturesVisible")); if (!pendingVisible) signature.setMissingSignatures(new TreeSet<>()); @@ -168,7 +168,7 @@ private boolean hideSignatures(Map params, Signature signature, return pendingVisible && signaturesVisible; } - private boolean isVisible(Signature signature, String currentUserName, String signaturesVisibleParam) { + private boolean isVisible(Signature2 signature, String currentUserName, String signaturesVisibleParam) { switch (SignaturesVisible.ofValue(signaturesVisibleParam)) { case IF_SIGNATORY: return signature.hasSigned(currentUserName) || signature.isSignatory(currentUserName); @@ -268,8 +268,8 @@ private Set getSet(Map params, String key) { return value == null || value.trim().isEmpty() ? new TreeSet<>() : new TreeSet<>(asList(value.split("[;, ]+"))); } - private Signature sync(Signature signature, Set signers) { - Signature loaded = Signature.fromBandana(this.bandanaManager, signature.getKey()); + private Signature2 sync(Signature2 signature, Set signers) { + Signature2 loaded = Signature2.fromBandana(this.bandanaManager, signature.getKey()); if (loaded != null) { signature.setSignatures(loaded.getSignatures()); boolean save = false; @@ -306,9 +306,9 @@ private Signature sync(Signature signature, Set signers) { return signature; } - private void save(Signature signature) { + private void save(Signature2 signature) { if (signature.hasMissingSignatures()) { - Signature.toBandana(bandanaManager, signature); + Signature2.toBandana(bandanaManager, signature); } } @@ -322,7 +322,7 @@ public OutputType getOutputType() { return OutputType.BLOCK; } - protected String getMailto(Collection profiles, String subject, boolean signed, Signature signature) { + protected String getMailto(Collection profiles, String subject, boolean signed, Signature2 signature) { if (profiles == null || profiles.isEmpty()) return null; Collection profilesWithMail = profiles.stream().filter(contextHelper::hasEmail).collect(toList()); StringBuilder ret = new StringBuilder("mailto:"); diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index e084eed..dac0c95 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -9,6 +9,7 @@ import java.util.Set; import java.util.TreeSet; +@Deprecated public class Signature implements Serializable, Cloneable { private static final long serialVersionUID = 1L; diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java index e57dfad..f57d350 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature2.java @@ -68,11 +68,19 @@ public static Signature2 fromBandana(BandanaManager mgr, String key) { throw new IllegalArgumentException("Value is null in Bandana???"); } - if (value instanceof Signature2) { + if (value instanceof Signature) { // required for downward compatibility - update for next time. - Signature2 signature = (Signature2) value; - toBandana(mgr, key, signature); - return signature; + Signature signature = (Signature) value; + Signature2 sig = new Signature2(signature.getPageId(), signature.getBody(), signature.getTitle()); + sig.setSignatures(signature.getSignatures()); + sig.setMaxSignatures(signature.getMaxSignatures()); + sig.setHash(signature.getHash()); + sig.setKey(signature.getKey()); + sig.setNotify(signature.getNotify()); + sig.setMissingSignatures(signature.getMissingSignatures()); + sig.setVisibilityLimit(signature.getVisibilityLimit()); + toBandana(mgr, key, sig); + return sig; } if (value instanceof String) { diff --git a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java index 587b492..1feaf23 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/rest/DigitalSignatureService.java @@ -20,13 +20,16 @@ import com.atlassian.velocity.htmlsafe.HtmlSafe; import com.baloise.confluence.digitalsignature.ContextHelper; import com.baloise.confluence.digitalsignature.Markdown; -import com.baloise.confluence.digitalsignature.Signature; +import com.baloise.confluence.digitalsignature.Signature2; import org.apache.velocity.tools.generic.DateTool; -import org.jsoup.helper.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -87,9 +90,9 @@ public Response sign(@QueryParam("key") final String key, @Context UriInfo uriInfo) { ConfluenceUser confluenceUser = AuthenticatedUserThreadLocal.get(); String userName = confluenceUser.getName(); - Signature signature = Signature.fromBandana(bandanaManager, key); + Signature2 signature = Signature2.fromBandana(bandanaManager, key); - if (signature == null || StringUtil.isBlank(userName)) { + if (signature == null || userName == null || userName.trim().isEmpty()) { log.error("Both, a signature and a user name are required to call this method.", new NullPointerException(signature == null ? "signature" : "userName")); return Response.noContent().build(); @@ -102,7 +105,7 @@ public Response sign(@QueryParam("key") final String key, .build(); } - Signature.toBandana(bandanaManager, key, signature); + Signature2.toBandana(bandanaManager, key, signature); String baseUrl = settingsManager.getGlobalSettings().getBaseUrl(); for (String notifiedUser : signature.getNotify()) { notify(notifiedUser, confluenceUser, signature, baseUrl); @@ -118,7 +121,7 @@ public Response sign(@QueryParam("key") final String key, return temporaryRedirect(pageUri).build(); } - private void notify(final String notifiedUser, ConfluenceUser signedUser, final Signature signature, final String baseUrl) { + private void notify(final String notifiedUser, ConfluenceUser signedUser, final Signature2 signature, final String baseUrl) { try { UserProfile notifiedUserProfile = contextHelper.getProfileNotNull(userManager, notifiedUser); @@ -171,7 +174,7 @@ private void notify(final String notifiedUser, ConfluenceUser signedUser, final @Produces("text/html; charset=UTF-8") @HtmlSafe public String export(@QueryParam("key") final String key) { - Signature signature = Signature.fromBandana(bandanaManager, key); + Signature2 signature = Signature2.fromBandana(bandanaManager, key); if (signature == null) { log.error("A signature is required to call this method.", new NullPointerException("signature")); @@ -200,7 +203,7 @@ public Response emails(@QueryParam("key") final String key, @QueryParam("signed") final boolean signed, @QueryParam("emailOnly") final boolean emailOnly, @Context UriInfo uriInfo) { - Signature signature = Signature.fromBandana(bandanaManager, key); + Signature2 signature = Signature2.fromBandana(bandanaManager, key); if (signature == null) { log.error("A signature is required to call this method.", new NullPointerException("signature")); diff --git a/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java b/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java index 8bebb10..8a0aa02 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/DigitalSignatureMacroTest.java @@ -13,7 +13,7 @@ import static org.mockito.Mockito.when; class DigitalSignatureMacroTest { - private final Signature signature = new Signature(1, "test", "title"); + private final Signature2 signature = new Signature2(1, "test", "title"); private final BootstrapManager bootstrapManager = mock(BootstrapManager.class); private final BandanaManager bandana = mock(BandanaManager.class); diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java index b33aca5..9542834 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java @@ -18,7 +18,7 @@ class SignatureSerialisationTest { @Test void deserialize() throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(getClass().getResourceAsStream("/signature.ser")); - Signature signature = (Signature) in.readObject(); + Signature2 signature = (Signature2) in.readObject(); in.close(); assertAll( @@ -29,16 +29,16 @@ void deserialize() throws IOException, ClassNotFoundException { () -> assertEquals(9999, signature.getSignatures().get("signed1").getTime()), // assert we can still read the old gson serialization - () -> assertEquals(signature, Signature.deserialize(SIG_JSON)), + () -> assertEquals(signature, Signature2.deserialize(SIG_JSON)), // assert that deserialization of the serialization results in the original Signature - () -> assertEquals(signature, Signature.deserialize(signature.serialize())) + () -> assertEquals(signature, Signature2.deserialize(signature.serialize())) ); } @Test void serialize() throws IOException, ClassNotFoundException { - Signature signature = new Signature(123L, "body", "title"); + Signature2 signature = new Signature2(123L, "body", "title"); signature.getNotify().add("notify1"); signature.getMissingSignatures().add("missing1"); signature.getMissingSignatures().add("missing2"); diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java index a43c8b3..3c2212c 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java @@ -18,7 +18,7 @@ class SignatureTest { class SerializationTest { @Test void serialize_empty() { - Signature signature = new Signature(); + Signature2 signature = new Signature2(); String json = signature.serialize(); @@ -27,7 +27,7 @@ void serialize_empty() { @Test void serialize_initializedObject() { - Signature signature = new Signature(42L, "body text", "title text"); + Signature2 signature = new Signature2(42L, "body text", "title text"); signature.sign("max.mustermann"); signature.setMissingSignatures(Collections.singleton("max.muster")); signature.setNotify(Collections.singleton("max.meier")); @@ -39,20 +39,20 @@ void serialize_initializedObject() { @Test void deserialize_empty() { - assertNull(Signature.deserialize(null)); - assertNull(Signature.deserialize("")); + assertNull(Signature2.deserialize(null)); + assertNull(Signature2.deserialize("")); } @Test void serializeAndDeserialize() { - Signature signature = new Signature(42L, "body text", "title text"); + Signature2 signature = new Signature2(42L, "body text", "title text"); signature.sign("max.mustermann"); signature.setMissingSignatures(Collections.singleton("max.muster")); signature.setNotify(Collections.singleton("max.meier")); String json = signature.serialize(); - Signature restoredSignature = Signature.deserialize(json); + Signature2 restoredSignature = Signature2.deserialize(json); assertEquals("{\"key\":\"signature.752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"hash\":\"752b4cc6b4933fc7f0a6efa819c1bcc440c32155457e836d99d1bfe927cc22f5\",\"pageId\":42,\"title\":\"title text\",\"body\":\"body text\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{},\"missingSignatures\":[\"max.muster\"],\"notify\":[\"max.meier\"]}", json); assertEquals(signature, restoredSignature); @@ -62,7 +62,7 @@ void serializeAndDeserialize() { @Nested class BandanaWrapperTest { private final BandanaManager bandana = mock(DefaultBandanaManager.class); - private final Signature signature = new Signature(1, "test", "title"); + private final Signature2 signature = new Signature2(1, "test", "title"); @Test void toBandanaFromBandana_readAsWritten() { @@ -70,42 +70,42 @@ void toBandanaFromBandana_readAsWritten() { ArgumentCaptor objectCapator = ArgumentCaptor.forClass(Object.class); String key = signature.getKey(); - assertNull(Signature.fromBandana(bandana, key), "Should not be there yet."); + assertNull(Signature2.fromBandana(bandana, key), "Should not be there yet."); doNothing().when(bandana).setValue(any(), stringCapator.capture(), objectCapator.capture()); when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); - Signature.toBandana(bandana, signature); + Signature2.toBandana(bandana, signature); assertEquals(key, stringCapator.getValue()); assertEquals(signature.serialize(), objectCapator.getValue()); when(bandana.getValue(any(), any())).thenCallRealMethod(); when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature); - assertEquals(signature, Signature.fromBandana(bandana, signature.getKey())); + assertEquals(signature, Signature2.fromBandana(bandana, signature.getKey())); } @Test void fromBandana_signature_signature() { String key = signature.getKey(); - assertNull(Signature.fromBandana(bandana, key), "Should not be there yet."); + assertNull(Signature2.fromBandana(bandana, key), "Should not be there yet."); when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); when(bandana.getValue(any(), any())).thenCallRealMethod(); when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature); - assertEquals(signature, Signature.fromBandana(bandana, signature.getKey())); + assertEquals(signature, Signature2.fromBandana(bandana, signature.getKey())); } @Test void fromBandana_string_signature() { String key = signature.getKey(); - assertNull(Signature.fromBandana(bandana, key), "Should not be there yet."); + assertNull(Signature2.fromBandana(bandana, key), "Should not be there yet."); when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); when(bandana.getValue(any(), any())).thenCallRealMethod(); when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature.serialize()); - assertEquals(signature, Signature.fromBandana(bandana, signature.getKey())); + assertEquals(signature, Signature2.fromBandana(bandana, signature.getKey())); } } } From f16b769d66675535a17aa096ba3eeed065df7647 Mon Sep 17 00:00:00 2001 From: tiliavir Date: Sun, 10 Sep 2023 11:10:28 +0200 Subject: [PATCH 15/16] #82: fixes tests and adds documentation --- .../digitalsignature/Signature.java | 8 ++++- .../SignatureSerialisationTest.java | 36 ++++++++++++++++--- .../digitalsignature/SignatureTest.java | 5 +-- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java index dac0c95..c30face 100644 --- a/src/main/java/com/baloise/confluence/digitalsignature/Signature.java +++ b/src/main/java/com/baloise/confluence/digitalsignature/Signature.java @@ -9,6 +9,12 @@ import java.util.Set; import java.util.TreeSet; +/** + * This class is deprecated and should no longer be used except for downwards compatibility, i.e. reading values from + * Bandana that were written with an older version. + *
      + * Use @{@link com.baloise.confluence.digitalsignature.Signature2} instead. + */ @Deprecated public class Signature implements Serializable, Cloneable { @@ -199,6 +205,6 @@ public boolean hasMissingSignatures() { @Override public Signature clone() throws CloneNotSupportedException{ - return (Signature) super.clone(); + return (Signature) super.clone(); } } diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java index 9542834..2332b4b 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java @@ -1,6 +1,8 @@ package com.baloise.confluence.digitalsignature; +import com.atlassian.bandana.BandanaManager; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.IOException; import java.io.ObjectInputStream; @@ -9,20 +11,35 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; +import java.util.HashSet; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class SignatureSerialisationTest { public static final String SIG_JSON = "{\"key\":\"signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"hash\":\"a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3\",\"pageId\":123,\"title\":\"title\",\"body\":\"body\",\"maxSignatures\":-1,\"visibilityLimit\":-1,\"signatures\":{\"signed1\":\"1970-01-01T01:00:09CET\"},\"missingSignatures\":[\"missing1\",\"missing2\"],\"notify\":[\"notify1\"]}"; @Test void deserialize() throws IOException, ClassNotFoundException { - ObjectInputStream in = new ObjectInputStream(getClass().getResourceAsStream("/signature.ser")); - Signature2 signature = (Signature2) in.readObject(); - in.close(); + String signatureKey = "signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3"; + Signature2 signature; + try(ObjectInputStream in = new ObjectInputStream(getClass().getResourceAsStream("/signature.ser"))) { + + HashSet keys = new HashSet<>(); + keys.add(signatureKey); + BandanaManager mgr = mock(BandanaManager.class); + when(mgr.getValue(any(), any())).thenReturn(in.readObject()); + when(mgr.getKeys(any())).thenReturn(keys); + + signature = Signature2.fromBandana(mgr, signatureKey); + } + + assertNotNull(signature); assertAll( - () -> assertEquals("signature.a077cdcc5bfcf275fe447ae2c609c1c361331b4e90cb85909582e0d824cbc5b3", signature.getKey()), + () -> assertEquals(signatureKey, signature.getKey()), () -> assertEquals("[missing1, missing2]", signature.getMissingSignatures().toString()), () -> assertEquals(1, signature.getSignatures().size()), () -> assertTrue(signature.getSignatures().containsKey("signed1")), @@ -52,6 +69,17 @@ void serialize() throws IOException, ClassNotFoundException { // assert the serialization we just wrote can be deserialized ObjectInputStream in = new ObjectInputStream(Files.newInputStream(path)); assertEquals(signature, in.readObject()); + } + + @Test + void deserializeHistoricalRecord() throws IOException, ClassNotFoundException { + Signature signature = new Signature(123L, "body", "title"); + signature.getNotify().add("notify1"); + signature.getMissingSignatures().add("missing1"); + signature.getMissingSignatures().add("missing2"); + signature.getSignatures().put("signed1", new Date(9999)); + + ObjectInputStream in; // assert the historically serialized class can still be deserialized in = new ObjectInputStream(this.getClass().getResourceAsStream("/signature.ser")); diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java index 3c2212c..84023b7 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureTest.java @@ -63,6 +63,7 @@ void serializeAndDeserialize() { class BandanaWrapperTest { private final BandanaManager bandana = mock(DefaultBandanaManager.class); private final Signature2 signature = new Signature2(1, "test", "title"); + private final Signature signatureOld = new Signature(1, "test", "title"); @Test void toBandanaFromBandana_readAsWritten() { @@ -80,7 +81,7 @@ void toBandanaFromBandana_readAsWritten() { assertEquals(signature.serialize(), objectCapator.getValue()); when(bandana.getValue(any(), any())).thenCallRealMethod(); - when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature); + when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature.serialize()); assertEquals(signature, Signature2.fromBandana(bandana, signature.getKey())); } @@ -91,7 +92,7 @@ void fromBandana_signature_signature() { when(bandana.getKeys(any())).thenReturn(Collections.singletonList(key)); when(bandana.getValue(any(), any())).thenCallRealMethod(); - when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signature); + when(bandana.getValue(any(), eq(key), eq(true))).thenReturn(signatureOld); assertEquals(signature, Signature2.fromBandana(bandana, signature.getKey())); } From 49243e8e1d5d7936f9b0f96bb03f422255b78d82 Mon Sep 17 00:00:00 2001 From: tiliavir Date: Sun, 10 Sep 2023 11:12:18 +0200 Subject: [PATCH 16/16] #82: updates docker documentation --- docs/docker.md | 18 ++++++++++++++---- .../SignatureSerialisationTest.java | 1 - 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 6012ac2..ac56309 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -3,8 +3,18 @@ Setup following a [tutorial from coffeetime.solutions]( http://coffeetime.soluti ```bash mkdir -p $HOME/docker/volumes/postgres mkdir -p $HOME/docker/volumes/confluence -docker run --name postgres -v $HOME/docker/volumes/postgres:/var/lib/postgresql/data -e POSTGRES_PASSWORD=mysecretpassword -d postgres -docker run --name=confluence -v $HOME/docker/volumes/confluence:/var/atlassian/application-data/confluence -d -p 8090:8090 -p 8091:8091 atlassian/confluence-server:latest +docker run --name postgres \ + -v $HOME/docker/volumes/postgres:/var/lib/postgresql/data \ + -e POSTGRES_PASSWORD=mysecretpassword \ + -d postgres +docker run --name=confluence \ + -v $HOME/docker/volumes/confluence:/var/atlassian/application-data/confluence \ + -d \ + -p 8090:8090 \ + -p 8091:8091 \ + -p 5005:5005 \ + -e JVM_SUPPORT_RECOMMENDED_ARGS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" \ + atlassian/confluence-server:latest docker inspect postgres | grep IPAddress # get the IP address of the postgres container ``` @@ -16,5 +26,5 @@ Start confluence setup and configure Postgres: ![](img/db.png) -Skip tutorial -Create new space "Test" +- Skip tutorial +- Create new space "Test" diff --git a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java index 2332b4b..8fe1be9 100644 --- a/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java +++ b/src/test/java/com/baloise/confluence/digitalsignature/SignatureSerialisationTest.java @@ -2,7 +2,6 @@ import com.atlassian.bandana.BandanaManager; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.io.IOException; import java.io.ObjectInputStream;