diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7d22ad0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/.settings
+/.vscode
+/target
+/.classpath
+/.project
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a716fd8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2018, ASERVO Software GmbH
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of ASERVO Software GmbH nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 195f0dc..36ea031 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,280 @@
-# jira-rest-extender
+[](https://www.tiburon.su)
+REST API Extender for JIRA
+================
+
+REST API for automated JIRA configuration with URMS
+
+Related Documentation
+---------------------
+
+* [Atlassian REST API design guidelines version 1](https://developer.atlassian.com/server/framework/atlassian-sdk/atlassian-rest-api-design-guidelines-version-1/)
+
+Resources
+---------
+
+All resources produce JSON (media type: `application/json`) results.
+
+### Settings
+
+Access important JIRA settings like the title, the base url, the mode
+etc.
+
+* #### `GET /rest/extender/1/settings`
+
+ Get JIRA application settings.
+
+ __Responses__
+
+ ![Status 200][status-200]
+
+ ```javascript
+ {
+ "baseurl": "http://localhost:2990/jira",
+ "mode": "private",
+ "title": "Your Company JIRA"
+ }
+ ```
+
+ ![Status 401][status-401]
+
+ Returned if the current user is not authenticated.
+
+ ![Status 403][status-403]
+
+ Returned if the current user is not an administrator.
+
+* #### `PUT /rest/extender/1/settings`
+
+ Set JIRA application settings.
+
+ __Request Body__
+
+ Media type: `application/json`
+
+ Content: Settings, for example:
+
+ ```javascript
+ {
+ "baseurl": "http://localhost:2990/jira",
+ "mode": "private",
+ "title": "Your Company JIRA"
+ }
+ ```
+
+ __Request Parameters__
+
+ None.
+
+ __Responses__
+
+ ![Status 200][status-200]
+
+ Returned if request could be executed without major exceptions.
+
+ The response will contain a list of errors that occurred while setting
+ some specific values such as a string that was too long, for example:
+
+ ```
+ {
+ "errorMessages": [
+ "The length of the application title must not exceed 255 characters"
+ ],
+ "errors": {}
+ }
+ ```
+
+ ![Status 401][status-401]
+
+ Returned if the current user is not authenticated.
+
+ ![Status 403][status-403]
+
+ Returned if the current user is not an administrator.
+
+### Licenses
+
+The JIRA license API is a bit weird and needs to be well understood.
+Just like in the web interface, different license keys can theoretically
+be set for each application (JIRA Core, JIRA Software, etc.). However,
+the entered license key is always set for all applications for which it
+is valid.
+
+For example:
+
+1. JIRA Core and JIRA Software each have their own license key. If a
+license key is now entered (in the web interface or via the REST API)
+with which both applications can be licensed, this license key is stored
+for both applications.
+
+2. JIRA Core and JIRA Software use a common license key. If a license
+key is now entered (in the web interface or via the REST API) with which
+only JIRA Core can be licensed, the license key is also only stored for
+JIRA Core.
+
+So again, an entered license key is always stored for all applications
+for which it is valid. The web interface might suggest that you can
+select the desired application, but this is not true.
+
+* #### `GET /rest/extender/1/licenses`
+
+ Get the license keys together with the application keys of the
+ applications using this license key.
+
+ __Responses__
+
+ ![Status 200][status-200]
+
+ ```javascript
+ {
+ "licenses": [
+ {
+ "key": "AAA...",
+ "applicationKeys": [
+ "jira-software"
+ ]
+ },
+ {
+ "key": "AAA...",
+ "applicationKeys": [
+ "jira-core",
+ "jira-servicedesk"
+ ]
+ }
+ ]
+ }
+ ```
+
+ ![Status 401][status-401]
+
+ Returned if the current user is not authenticated.
+
+ ![Status 403][status-403]
+
+ Returned if the current user is not an administrator.
+
+* #### `PUT /rest/extender/1/licenses`
+
+ Set a license by its license key.
+
+ __Request Body__
+
+ Media type: `text/plain`
+
+ Content: License key, for example:
+
+ ```
+ AAA...
+ ```
+
+ __Request Parameters__
+
+ | parameter | type | description |
+ | ----------- | --------- | ------------------------------------------------------------------------------ |
+ | `clear` | _boolean_ | Clear all licenses before setting the new license, optional, defaults to false |
+
+ __Responses__
+
+ ![Status 200][status-200]
+
+ ```javascript
+ {
+ "key": "AAA...",
+ "applicationKeys": [
+ "jira-core",
+ "jira-servicedesk",
+ "jira-software"
+ ]
+ }
+ ```
+
+ ![Status 401][status-401]
+
+ Returned if the current user is not authenticated.
+
+ ![Status 403][status-403]
+
+ Returned if the current user is not an administrator.
+
+### Syncronise Crowd User Directory
+
+* #### `GET /rest/extender/1/directory`
+
+ Get info about User Directories.
+
+ __Responses__
+
+ ![Status 200][status-200]
+
+ ```javascript
+ [
+ {
+ "id": 1,
+ "name": "Jira Internal Directory",
+ "active": true,
+ "createdDate": 1362038271308,
+ "updatedDate": 1362038271308,
+ "lowerName": "jira internal directory",
+ "description": "Jira default internal directory",
+ "type": "INTERNAL",
+ "implementationClass": "com.atlassian.crowd.directory.InternalDirectory",
+ "lowerImplementationClass": "com.atlassian.crowd.directory.internaldirectory",
+ "allowedOperations":["CREATE_GROUP", "CREATE_ROLE", "CREATE_USER", "DELETE_GROUP", "DELETE_ROLE", "DELETE_USER", "UPDATE_GROUP",…],
+ "attributes":{
+ "user_encryption_method": "atlassian-security"
+ },
+ "empty": false,
+ "keys":["user_encryption_method"],
+ "encryptionType": "atlassian-security"
+ }
+ ]
+ ```
+
+ ![Status 401][status-401]
+
+ Returned if the current user is not authenticated.
+
+ ![Status 403][status-403]
+
+ Returned if the current user is not an administrator.
+
+* #### `PUT /rest/extender/1/directory`
+
+ Start sycronyse User Directory.
+
+ __Request Parameters__
+
+ | parameter | type | description |
+ | --------- | ------ | ----------------------------------------------------- |
+ | `id` | _long_ | id of the User Directory, which should be syncronised |
+
+ __Responses__
+
+ ![Status 200][status-200]
+
+ Returned if request could be executed without major exceptions.
+
+ The response will contain a list of errors that occurred while setting
+ some specific values such as a string that was too long, for example:
+
+ ```
+ {
+ "message": "parameter should not be null!",
+ "status-code": 500,
+ "stack-trace": full_stack_trace_of_the_application
+ }
+ ```
+ ![Status 401][status-401]
+
+ Returned if the current user is not authenticated.
+
+ ![Status 403][status-403]
+
+ Returned if the current user is not an administrator.
+
+
+[status-200]: https://img.shields.io/badge/status-200-brightgreen.svg
+[status-400]: https://img.shields.io/badge/status-400-red.svg
+[status-401]: https://img.shields.io/badge/status-401-red.svg
+[status-403]: https://img.shields.io/badge/status-403-red.svg
+[status-404]: https://img.shields.io/badge/status-404-red.svg
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..fadeea6
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,533 @@
+
+
+
+ 4.0.0
+
+ su.tiburon.atlassian.jira
+ rest-extender
+ 1.1.0
+ atlassian-plugin
+
+ REST API extender for JIRA
+ REST API extender for JIRA.
+
+
+
+ BSD 3-Clause License
+ https://opensource.org/licenses/BSD-3-Clause
+
+
+
+
+ Github
+ https://github.com/tiburon-777/rest-extender/issues
+
+
+
+ scm:git:git://github.com/tiburon-777/rest-extender.git
+ scm:git:git@github.com:tiburon-777/rest-extender.git
+ https://github.com/tiburon-777/rest-extender
+
+
+
+ OCTOPUS CodeWare
+ http://www.tiburon.su/
+
+
+
+
+ Andrey Ivanov
+ ya@tiburon.su
+ OCTOPUS CodeWare
+ https://www.tiburon.su
+
+
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+ UTF-8
+
+
+ 8.0.1
+
+ 7.5.0
+ 8.0.2
+ 8.0.2
+ 6.2.0
+ 3.1.0
+ 5.0.1
+ 2.0.0
+ 2.1.5
+ 2.10.2-rc04
+ 2.15.3
+
+
+ 1.7.9
+ 2.4
+ 1.1.1
+ 2.1
+
+
+ 4.12
+ 1.10.19
+ 0.8.1
+ 4.3.0
+
+
+
+
+
+
+ com.atlassian.jira
+ jira-api
+ ${jira.version}
+ provided
+
+
+ jta
+ jta
+
+
+
+
+
+ com.atlassian.jira
+ jira-core
+ ${jira.version}
+ provided
+
+
+ jndi
+ jndi
+
+
+ jta
+ jta
+
+
+
+
+
+ com.atlassian.jira.plugins
+ project-templates-api
+ ${atlassian.project.templates.version}
+ provided
+
+
+
+ com.atlassian.jira
+ jira-rest-api
+ ${jira.version}
+ provided
+
+
+
+ com.atlassian.plugins.rest
+ atlassian-rest-common
+ ${atlassian.plugins.rest.version}
+ provided
+
+
+
+ com.atlassian.plugins.rest
+ atlassian-rest-doclet
+ ${atlassian.plugins.rest.version}
+ provided
+
+
+
+ com.atlassian.plugin
+ atlassian-spring-scanner-annotation
+ ${atlassian.spring.scanner.version}
+ provided
+
+
+
+ com.atlassian.templaterenderer
+ atlassian-template-renderer-api
+ 3.0.0
+ provided
+
+
+
+ com.atlassian.sal
+ sal-api
+ ${atlassian.sal.version}
+ provided
+
+
+
+ com.atlassian.crowd
+ crowd-api
+ ${atlassian.crowdapi.version}
+ provided
+
+
+
+ com.atlassian.upm
+ plugin-license-storage-lib
+ ${upm.license.compatibility.version}
+
+
+
+ com.atlassian.upm
+ plugin-license-storage-plugin
+ ${upm.license.compatibility.version}
+ provided
+
+
+
+ com.atlassian.upm
+ licensing-api
+ ${upm.license.compatibility.version}
+ provided
+
+
+
+ com.atlassian.upm
+ upm-api
+ ${upm.license.compatibility.version}
+ provided
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ provided
+
+
+
+ javax.ws.rs
+ jsr311-api
+ ${javax.ws.version}
+ provided
+
+
+
+ javax.xml.bind
+ jaxb-api
+ ${javax.xml.version}
+ provided
+
+
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+
+ com.atlassian.jira
+ jira-tests
+ ${jira.version}
+ test
+
+
+
+
+
+
+ com.atlassian.maven.plugins
+ jira-maven-plugin
+ ${atlassian.amps.version}
+ true
+
+ ${jira.version}
+ false
+ false
+ false
+
+ true
+ true
+ false
+
+ false
+
+
+
+ jira-software
+ ${jira.version}
+
+
+
+
+ com.atlassian.labs.plugins
+ quickreload
+ ${atlassian.quick.reload.version}
+
+
+
+ ${project.groupId}.${project.artifactId}
+ *
+
+ org.springframework.*;resolution:="optional",
+ org.eclipse.gemini.*;resolution:="optional",
+ sun.misc;resolution:=optional,
+ *
+
+
+
+
+
+ crowd
+ crowd
+ 3.1.3
+
+
+ confluence
+ confluence
+ 6.8.0
+
+
+
+
+
+
+ com.atlassian.plugin
+ atlassian-spring-scanner-maven-plugin
+ ${atlassian.spring.scanner.version}
+
+
+
+ atlassian-spring-scanner
+
+ process-classes
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+
+ org.eluder.coveralls
+ coveralls-maven-plugin
+ ${coveralls.version}
+
+
+
+
+
+
+ sonatype-nexus-snapshots
+ Sonatype Nexus snapshot repository
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+
+ sonatype-nexus-staging
+ Sonatype Nexus release staging repository
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+ release
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ deploy
+
+ sign
+
+
+
+
+ ${gpg.keyname}
+ ${gpg.passphrase}
+ ${gpg.executable}
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 2.8.2
+
+
+ true
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.8
+ true
+
+
+ default-deploy
+ deploy
+
+ deploy
+
+
+
+
+ sonatype-nexus-staging
+ https://oss.sonatype.org/
+ true
+
+
+
+
+
+
+
+ sources
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.0.1
+
+
+ attach-sources
+ install
+
+ jar-no-fork
+
+
+
+
+
+
+
+
+
+ javadoc
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.0.1
+
+
+ attach-javadocs
+ install
+
+ jar
+
+
+
+
+
+
+
+
+
+
+
+ atlassian-public
+ https://packages.atlassian.com/maven/repository/public
+
+ true
+ never
+ warn
+
+
+ true
+ warn
+
+
+
+
+ sonatype-oss
+ https://oss.sonatype.org/content/groups/public
+
+ true
+ never
+ warn
+
+
+ true
+ warn
+
+
+
+
+
+
+ atlassian-public
+ https://packages.atlassian.com/maven/repository/public
+
+ true
+ never
+ warn
+
+
+ true
+ warn
+
+
+
+
+ sonatype-oss
+ https://oss.sonatype.org/content/groups/public
+
+ true
+ never
+ warn
+
+
+ true
+ warn
+
+
+
+
+
diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraApplicationHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraApplicationHelper.java
new file mode 100644
index 0000000..8993c67
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/JiraApplicationHelper.java
@@ -0,0 +1,160 @@
+package su.tiburon.atlassian.jira;
+
+import com.atlassian.jira.bc.license.JiraLicenseService;
+import com.atlassian.jira.bc.license.JiraLicenseService.ValidationResult;
+import com.atlassian.jira.config.properties.APKeys;
+import com.atlassian.jira.config.properties.ApplicationProperties;
+import com.atlassian.jira.license.JiraLicenseManager;
+import com.atlassian.jira.license.LicenseDetails;
+import com.atlassian.jira.util.UrlValidator;
+import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
+import com.google.common.collect.Lists;
+import com.opensymphony.util.TextUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+import com.atlassian.crowd.embedded.api.CrowdDirectoryService;
+import com.atlassian.crowd.embedded.api.Directory;
+
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import java.util.Collection;
+import java.util.List;
+
+@Component
+public class JiraApplicationHelper {
+
+ @ComponentImport
+ private final ApplicationProperties applicationProperties;
+
+ private final JiraI18nHelper i18nHelper;
+
+ @ComponentImport
+ private final JiraLicenseManager licenseManager;
+
+ @ComponentImport
+ private final JiraLicenseService licenseService;
+
+ @ComponentImport
+ private final CrowdDirectoryService crowdDirectoryService;
+
+ /**
+ * Constructor.
+ *
+ * @param applicationProperties injected {@link ApplicationProperties}
+ * @param i18nHelper injected {@link JiraI18nHelper}
+ * @param licenseManager injected {@link JiraLicenseManager}
+ * @param licenseService injected {@link JiraLicenseService}
+ * @param crowdDirectoryService injected {@link CrowdDirectoryService}
+ */
+ @Inject
+ public JiraApplicationHelper(
+ final ApplicationProperties applicationProperties,
+ final JiraI18nHelper i18nHelper,
+ final JiraLicenseManager licenseManager,
+ final JiraLicenseService licenseService,
+ final CrowdDirectoryService crowdDirectoryService) {
+
+ this.applicationProperties = applicationProperties;
+ this.i18nHelper = i18nHelper;
+ this.licenseManager = licenseManager;
+ this.licenseService = licenseService;
+ this.crowdDirectoryService = crowdDirectoryService;
+ }
+
+
+ public void syncDir(@Nonnull final Long dirId ) {
+ crowdDirectoryService.synchroniseDirectory(dirId);
+ }
+
+ public List getDir() {
+ return crowdDirectoryService.findAllDirectories();
+ }
+
+ public String getBaseUrl() {
+ return applicationProperties.getString(APKeys.JIRA_BASEURL);
+ }
+
+ public void setBaseUrl(
+ @Nonnull final String baseUrl) {
+
+ if (!UrlValidator.isValid(baseUrl)) {
+ throw new IllegalArgumentException(i18nHelper.getText("admin.errors.you.must.set.a.valid.base.url"));
+ }
+
+ applicationProperties.setString(APKeys.JIRA_BASEURL, baseUrl);
+ }
+
+ public String getMode() {
+ return applicationProperties.getString(APKeys.JIRA_MODE);
+ }
+
+ public void setMode(
+ @Nonnull final String mode) {
+
+ if (!mode.equalsIgnoreCase("public") && !mode.equalsIgnoreCase("private")) {
+ throw new IllegalArgumentException("Invalid mode");
+ }
+
+ if (mode.equalsIgnoreCase("public") && hasExternalUserManagement()) {
+ throw new IllegalArgumentException(i18nHelper.getText("admin.errors.invalid.mode.externalUM.combination"));
+ }
+
+ applicationProperties.setString(APKeys.JIRA_MODE, mode);
+ }
+
+ public String getTitle() {
+ return applicationProperties.getString(APKeys.JIRA_TITLE);
+ }
+
+ public void setTitle(
+ @Nonnull final String title) {
+
+ if (!TextUtils.stringSet(title)) {
+ throw new IllegalArgumentException(i18nHelper.getText("admin.errors.you.must.set.an.application.title"));
+ }
+
+ if (StringUtils.length(title) > 255) {
+ throw new IllegalArgumentException(i18nHelper.getText("admin.errors.invalid.length.of.an.application.title"));
+ }
+
+ applicationProperties.setString(APKeys.JIRA_TITLE, title);
+ }
+
+ private boolean hasExternalUserManagement() {
+ return applicationProperties.getOption(APKeys.JIRA_OPTION_USER_EXTERNALMGT);
+ }
+
+ /**
+ * Get all licenses.
+ *
+ * @return licenses details
+ */
+ public Collection getLicenses() {
+ return Lists.newArrayList(licenseManager.getLicenses());
+ }
+
+ /**
+ * Set a new license key and clear all licenses before if wanted.
+ *
+ * @param key the license key
+ * @param clear whether to remove all licenses before setting the new license
+ * @return license details
+ */
+ public LicenseDetails setLicense(
+ final String key,
+ boolean clear) {
+
+ final ValidationResult validationResult = licenseService.validate(i18nHelper.getI18nHelper(), key);
+
+ if (validationResult.getErrorCollection().hasAnyErrors()) {
+ throw new IllegalArgumentException("Specified license was invalid.");
+ }
+
+ final String licenseString = validationResult.getLicenseString();
+
+ return clear
+ ? licenseManager.clearAndSetLicenseNoEvent(licenseString)
+ : licenseManager.setLicenseNoEvent(licenseString);
+ }
+
+}
diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraI18nHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraI18nHelper.java
new file mode 100644
index 0000000..fa76961
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/JiraI18nHelper.java
@@ -0,0 +1,59 @@
+package su.tiburon.atlassian.jira;
+
+import com.atlassian.jira.user.preferences.PreferenceKeys;
+import com.atlassian.jira.util.I18nHelper;
+import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
+import com.opensymphony.module.propertyset.PropertySet;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import java.util.Locale;
+
+@Component
+public class JiraI18nHelper {
+
+ @ComponentImport
+ private final I18nHelper.BeanFactory i18nBeanFactory;
+
+ private final JiraUserHelper userHelper;
+
+ /**
+ * Constructor.
+ *
+ * @param i18nBeanFactory injected {@link I18nHelper.BeanFactory}
+ * @param userHelper injected {@link JiraUserHelper}
+ */
+ @Inject
+ public JiraI18nHelper(
+ final I18nHelper.BeanFactory i18nBeanFactory,
+ final JiraUserHelper userHelper) {
+
+ this.i18nBeanFactory = i18nBeanFactory;
+ this.userHelper = userHelper;
+ }
+
+ public String getText(
+ final String key) {
+
+ return getI18nHelper().getText(key);
+ }
+
+ public I18nHelper getI18nHelper() {
+ return i18nBeanFactory.getInstance(getLocale());
+ }
+
+ Locale getLocale() {
+ final PropertySet userProperties = userHelper.getUserProperties();
+
+ if (userProperties != null) {
+ final String locale = userProperties.getString(PreferenceKeys.USER_LOCALE);
+
+ if (locale != null) {
+ return new Locale(locale);
+ }
+ }
+
+ return Locale.getDefault();
+ }
+
+}
diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraUserHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraUserHelper.java
new file mode 100644
index 0000000..ce01c97
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/JiraUserHelper.java
@@ -0,0 +1,75 @@
+package su.tiburon.atlassian.jira;
+
+import com.atlassian.jira.security.JiraAuthenticationContext;
+import com.atlassian.jira.user.ApplicationUser;
+import com.atlassian.jira.user.UserPropertyManager;
+import com.atlassian.jira.user.preferences.UserPreferencesManager;
+import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
+import com.opensymphony.module.propertyset.PropertySet;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+@Component
+public class JiraUserHelper {
+
+ @ComponentImport
+ private final JiraAuthenticationContext authenticationContext;
+
+ @ComponentImport
+ private final UserPropertyManager userPropertyManager;
+
+ /**
+ * Constructor.
+ *
+ * @param authenticationContext injected {@link JiraAuthenticationContext}
+ * @param userPropertyManager injected {@link UserPreferencesManager}
+ */
+ @Inject
+ public JiraUserHelper(
+ final JiraAuthenticationContext authenticationContext,
+ final UserPropertyManager userPropertyManager) {
+
+ this.authenticationContext = authenticationContext;
+ this.userPropertyManager = userPropertyManager;
+ }
+
+ /**
+ * Get logged in user.
+ *
+ * @return user
+ */
+ @Nullable
+ public ApplicationUser getLoggedInUser() {
+ return authenticationContext.getLoggedInUser();
+ }
+
+ /**
+ * Get the property set of the logged in user.
+ *
+ * @return property set
+ */
+ @Nullable
+ public PropertySet getUserProperties() {
+ return getUserProperties(getLoggedInUser());
+ }
+
+ /**
+ * Get the property set of the given user.
+ *
+ * @param user the application user
+ * @return property set
+ */
+ @Nullable
+ public PropertySet getUserProperties(
+ @Nullable final ApplicationUser user) {
+
+ if (user != null) {
+ return userPropertyManager.getPropertySet(user);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraWebAuthenticationHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraWebAuthenticationHelper.java
new file mode 100644
index 0000000..d146235
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/JiraWebAuthenticationHelper.java
@@ -0,0 +1,48 @@
+package su.tiburon.atlassian.jira;
+
+import com.atlassian.jira.security.GlobalPermissionManager;
+import com.atlassian.jira.user.ApplicationUser;
+import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+import static com.atlassian.jira.permission.GlobalPermissionKey.SYSTEM_ADMIN;
+
+@Component
+public class JiraWebAuthenticationHelper {
+
+ @ComponentImport
+ private final GlobalPermissionManager globalPermissionManager;
+
+ private final JiraUserHelper userHelper;
+
+ /**
+ * Constructor.
+ *
+ * @param userHelper the injected {@link JiraUserHelper}
+ * @param globalPermissionManager the injected {@link GlobalPermissionManager}
+ */
+ @Inject
+ public JiraWebAuthenticationHelper(
+ final GlobalPermissionManager globalPermissionManager,
+ final JiraUserHelper userHelper) {
+
+ this.globalPermissionManager = globalPermissionManager;
+ this.userHelper = userHelper;
+ }
+
+ public void mustBeSysAdmin() {
+ final ApplicationUser user = userHelper.getLoggedInUser();
+ if (user == null) {
+ throw new WebApplicationException(Response.Status.UNAUTHORIZED);
+ }
+
+ final boolean isSysAdmin = globalPermissionManager.hasPermission(SYSTEM_ADMIN, user);
+ if (!isSysAdmin) {
+ throw new WebApplicationException(Response.Status.FORBIDDEN);
+ }
+ }
+}
diff --git a/src/main/java/su/tiburon/atlassian/jira/bean/LicenseBean.java b/src/main/java/su/tiburon/atlassian/jira/bean/LicenseBean.java
new file mode 100644
index 0000000..1de2f62
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/bean/LicenseBean.java
@@ -0,0 +1,75 @@
+package su.tiburon.atlassian.jira.bean;
+
+import com.atlassian.application.api.ApplicationKey;
+import com.atlassian.jira.license.LicenseDetails;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Bean for {@link com.atlassian.jira.license.LicenseDetails} results in REST responses.
+ */
+@XmlRootElement(name = "license")
+public class LicenseBean {
+
+ @XmlElement
+ private final String key;
+
+ @XmlElement
+ private final Collection applicationKeys;
+
+ private LicenseBean(
+ final String key,
+ final Collection applicationKeys) {
+
+ this.key = key;
+ this.applicationKeys = applicationKeys;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public Collection getApplicationKeys() {
+ return applicationKeys.stream()
+ .map(ApplicationKey::valueOf)
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof LicenseBean) {
+ LicenseBean other = (LicenseBean) obj;
+
+ return Objects.equals(key, other.key)
+ && Objects.equals(applicationKeys, other.applicationKeys);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, applicationKeys);
+ }
+
+ /**
+ * Factory method for creating a bean from {@link LicenseDetails}.
+ *
+ * @param licenseDetail the license details
+ */
+ public static LicenseBean from(
+ final LicenseDetails licenseDetail) {
+
+ final String key = licenseDetail.getLicenseString();
+ final Collection applicationKeys = licenseDetail.getLicensedApplications().getKeys().stream()
+ .map(ApplicationKey::value)
+ .collect(Collectors.toSet());
+
+ return new LicenseBean(key, applicationKeys);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/su/tiburon/atlassian/jira/bean/LicensesBean.java b/src/main/java/su/tiburon/atlassian/jira/bean/LicensesBean.java
new file mode 100644
index 0000000..439face
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/bean/LicensesBean.java
@@ -0,0 +1,44 @@
+package su.tiburon.atlassian.jira.bean;
+
+import com.atlassian.jira.license.LicenseDetails;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+/**
+ * Bean for collection of {@link LicenseDetails} results in REST responses.
+ */
+@XmlRootElement(name = "licenses")
+public class LicensesBean {
+
+ @XmlElement
+ private final Collection licenses;
+
+ private LicensesBean(
+ final Collection licenses) {
+
+ this.licenses = licenses;
+ }
+
+ public Collection getLicenses() {
+ return licenses;
+ }
+
+ /**
+ * Factory method for creating a bean from a collection of {@link LicenseDetails}.
+ *
+ * @param licenseDetails the license details
+ */
+ public static LicensesBean from(
+ final Collection licenseDetails) {
+
+ final Collection licenses = licenseDetails.stream()
+ .map(LicenseBean::from)
+ .collect(Collectors.toList());
+
+ return new LicensesBean(licenses);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/su/tiburon/atlassian/jira/bean/SettingsBean.java b/src/main/java/su/tiburon/atlassian/jira/bean/SettingsBean.java
new file mode 100644
index 0000000..1f072cd
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/bean/SettingsBean.java
@@ -0,0 +1,62 @@
+package su.tiburon.atlassian.jira.bean;
+
+import su.tiburon.atlassian.jira.JiraApplicationHelper;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Bean for setting results in REST responses.
+ */
+@XmlRootElement(name = "settings")
+public class SettingsBean {
+
+ @XmlElement
+ private final String baseurl;
+
+ @XmlElement
+ private final String mode;
+
+ @XmlElement
+ private final String title;
+
+ /**
+ * The default constructor is needed for JSON request deserialization.
+ */
+ public SettingsBean() {
+ this.baseurl = null;
+ this.mode = null;
+ this.title = null;
+ }
+
+ public SettingsBean(
+ final String baseUrl,
+ final String mode,
+ final String title) {
+
+ this.baseurl = baseUrl;
+ this.mode = mode;
+ this.title = title;
+ }
+
+ public SettingsBean(
+ final JiraApplicationHelper applicationHelper) {
+
+ this.baseurl = applicationHelper.getBaseUrl();
+ this.mode = applicationHelper.getMode();
+ this.title = applicationHelper.getTitle();
+ }
+
+ public String getBaseUrl() {
+ return baseurl;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getMode() {
+ return mode;
+ }
+
+}
diff --git a/src/main/java/su/tiburon/atlassian/jira/rest/DirectoriesResource.java b/src/main/java/su/tiburon/atlassian/jira/rest/DirectoriesResource.java
new file mode 100644
index 0000000..2d5a073
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/rest/DirectoriesResource.java
@@ -0,0 +1,89 @@
+package su.tiburon.atlassian.jira.rest;
+
+import com.atlassian.jira.rest.api.util.ErrorCollection;
+import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
+import su.tiburon.atlassian.jira.JiraApplicationHelper;
+import su.tiburon.atlassian.jira.JiraWebAuthenticationHelper;
+import org.springframework.stereotype.Component;
+import com.atlassian.crowd.embedded.api.Directory;
+import org.codehaus.jackson.map.ObjectMapper;
+
+import java.util.List;
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+
+/**
+ * Settings resource to get the licenses.
+ */
+@Path("/directory")
+@AnonymousAllowed
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class DirectoriesResource {
+
+ private final JiraApplicationHelper applicationHelper;
+
+ private final JiraWebAuthenticationHelper webAuthenticationHelper;
+
+ /**
+ * Constructor.
+ *
+ * @param applicationHelper the injected {@link JiraApplicationHelper}
+ * @param webAuthenticationHelper the injected {@link JiraWebAuthenticationHelper}
+ */
+ @Inject
+ public DirectoriesResource(
+ final JiraApplicationHelper applicationHelper,
+ final JiraWebAuthenticationHelper webAuthenticationHelper) {
+
+ this.applicationHelper = applicationHelper;
+ this.webAuthenticationHelper = webAuthenticationHelper;
+ }
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response syncDir(@QueryParam("id") Long dirId) throws WebApplicationException {
+
+ webAuthenticationHelper.mustBeSysAdmin();
+
+ final ErrorCollection errorCollection = ErrorCollection.of();
+
+ if (dirId != null) {
+ try {
+ applicationHelper.syncDir(dirId);
+ } catch (Exception e) {
+ errorCollection.addErrorMessage(e.getMessage());
+ }
+ } else {
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
+ }
+ if (errorCollection.hasAnyErrors()) {
+ return Response.ok(errorCollection).build();
+ } else {
+ return Response.ok(null).build();
+ }
+ }
+
+ @GET
+ public Response getDir() {
+ webAuthenticationHelper.mustBeSysAdmin();
+
+ ObjectMapper mapper = new ObjectMapper();
+ final List directories = applicationHelper.getDir();
+ String jsn = "";
+ try {
+ jsn = mapper.writeValueAsString(directories);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return Response.ok(jsn).build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/su/tiburon/atlassian/jira/rest/LicensesResource.java b/src/main/java/su/tiburon/atlassian/jira/rest/LicensesResource.java
new file mode 100644
index 0000000..8f2c90d
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/rest/LicensesResource.java
@@ -0,0 +1,76 @@
+package su.tiburon.atlassian.jira.rest;
+
+import com.atlassian.jira.license.LicenseDetails;
+import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
+import su.tiburon.atlassian.jira.JiraApplicationHelper;
+import su.tiburon.atlassian.jira.JiraWebAuthenticationHelper;
+import su.tiburon.atlassian.jira.bean.LicenseBean;
+import su.tiburon.atlassian.jira.bean.LicensesBean;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Collection;
+
+/**
+ * Licenses resource to get the licenses.
+ */
+@Path("/licenses")
+@AnonymousAllowed
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class LicensesResource {
+
+ private final JiraApplicationHelper applicationHelper;
+
+ private final JiraWebAuthenticationHelper webAuthenticationHelper;
+
+ /**
+ * Constructor.
+ *
+ * @param applicationHelper the injected {@link JiraApplicationHelper}
+ * @param webAuthenticationHelper the injected {@link JiraWebAuthenticationHelper}
+ */
+ @Inject
+ public LicensesResource(
+ final JiraApplicationHelper applicationHelper,
+ final JiraWebAuthenticationHelper webAuthenticationHelper) {
+
+ this.applicationHelper = applicationHelper;
+ this.webAuthenticationHelper = webAuthenticationHelper;
+ }
+
+ @GET
+ public Response getLicenses() {
+ webAuthenticationHelper.mustBeSysAdmin();
+
+ final Collection licenseDetails = applicationHelper.getLicenses();
+ return Response.ok(LicensesBean.from(licenseDetails)).build();
+ }
+
+ @PUT
+ @Consumes(MediaType.TEXT_PLAIN)
+ public Response setLicense(
+ @QueryParam("clear") @DefaultValue("false") boolean clear,
+ final String licenseKey) throws WebApplicationException {
+
+ webAuthenticationHelper.mustBeSysAdmin();
+
+ if (licenseKey == null) {
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
+ }
+
+ final LicenseDetails licenseDetail = applicationHelper.setLicense(licenseKey, clear);
+ return Response.ok(LicenseBean.from(licenseDetail)).build();
+ }
+
+}
diff --git a/src/main/java/su/tiburon/atlassian/jira/rest/SettingsResource.java b/src/main/java/su/tiburon/atlassian/jira/rest/SettingsResource.java
new file mode 100644
index 0000000..801cc95
--- /dev/null
+++ b/src/main/java/su/tiburon/atlassian/jira/rest/SettingsResource.java
@@ -0,0 +1,91 @@
+package su.tiburon.atlassian.jira.rest;
+
+import com.atlassian.jira.rest.api.util.ErrorCollection;
+import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
+import su.tiburon.atlassian.jira.JiraApplicationHelper;
+import su.tiburon.atlassian.jira.JiraWebAuthenticationHelper;
+import su.tiburon.atlassian.jira.bean.SettingsBean;
+import org.springframework.stereotype.Component;
+
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * Settings resource to get the licenses.
+ */
+@Path("/settings")
+@AnonymousAllowed
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class SettingsResource {
+
+ private final JiraApplicationHelper applicationHelper;
+
+ private final JiraWebAuthenticationHelper webAuthenticationHelper;
+
+ /**
+ * Constructor.
+ *
+ * @param applicationHelper the injected {@link JiraApplicationHelper}
+ * @param webAuthenticationHelper the injected {@link JiraWebAuthenticationHelper}
+ */
+ @Inject
+ public SettingsResource(
+ final JiraApplicationHelper applicationHelper,
+ final JiraWebAuthenticationHelper webAuthenticationHelper) {
+
+ this.applicationHelper = applicationHelper;
+ this.webAuthenticationHelper = webAuthenticationHelper;
+ }
+
+ @GET
+ public Response getSettings() {
+ webAuthenticationHelper.mustBeSysAdmin();
+
+ final SettingsBean settingsBean = new SettingsBean(applicationHelper);
+ return Response.ok(settingsBean).build();
+ }
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response setSettings(
+ final SettingsBean settingsBean) {
+
+ webAuthenticationHelper.mustBeSysAdmin();
+
+ final ErrorCollection errorCollection = ErrorCollection.of();
+
+ if (settingsBean.getBaseUrl() != null) {
+ try {
+ applicationHelper.setBaseUrl(settingsBean.getBaseUrl());
+ } catch (Exception e) {
+ errorCollection.addErrorMessage(e.getMessage());
+ }
+ }
+
+ if (settingsBean.getMode() != null) {
+ try {
+ applicationHelper.setMode(settingsBean.getMode());
+ } catch (Exception e) {
+ errorCollection.addErrorMessage(e.getMessage());
+ }
+ }
+
+ if (settingsBean.getTitle() != null) {
+ try {
+ applicationHelper.setTitle(settingsBean.getTitle());
+ } catch (Exception e) {
+ errorCollection.addErrorMessage(e.getMessage());
+ }
+ }
+
+ return Response.ok(errorCollection).build();
+ }
+
+}
diff --git a/src/main/resources/META-INF/spring/spring-scanner.xml b/src/main/resources/META-INF/spring/spring-scanner.xml
new file mode 100644
index 0000000..d91305f
--- /dev/null
+++ b/src/main/resources/META-INF/spring/spring-scanner.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml
new file mode 100644
index 0000000..6f2f630
--- /dev/null
+++ b/src/main/resources/atlassian-plugin.xml
@@ -0,0 +1,21 @@
+
+
+ ${project.description}
+ ${project.version}
+
+ images/logo_octo_375_293.png
+ images/logo_octo_375_293.png
+
+
+
+
+
+ com.atlassian.auiplugin:ajs
+ com.atlassian.auiplugin:aui-experimental-expander
+
+ atl.general
+
+
+
+
+
diff --git a/src/main/resources/images/logo_octo_375_293.png b/src/main/resources/images/logo_octo_375_293.png
new file mode 100644
index 0000000..543fe87
Binary files /dev/null and b/src/main/resources/images/logo_octo_375_293.png differ