All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.almworks.jira.structure.api.util.StructureUtil Maven / Gradle / Ivy

The newest version!
package com.almworks.jira.structure.api.util;

import com.almworks.integers.*;
import com.almworks.jira.structure.api.attribute.*;
import com.almworks.jira.structure.api.auth.StructureAuth;
import com.almworks.jira.structure.api.error.StructureException;
import com.almworks.jira.structure.api.i18n.JiraI18n;
import com.almworks.jira.structure.api.item.ItemIdentity;
import com.almworks.jira.structure.api.permissions.PermissionLevel;
import com.almworks.jira.structure.api.permissions.PermissionRule;
import com.almworks.jira.structure.api.row.StructureRow;
import com.almworks.jira.structure.api.structure.Structure;
import com.almworks.jira.structure.api.structure.StructureManager;
import com.atlassian.annotations.Internal;
import com.atlassian.crowd.embedded.api.Group;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.jira.config.properties.ApplicationProperties;
import com.atlassian.jira.issue.*;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.I18nHelper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.opensymphony.workflow.loader.ActionDescriptor;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;
import static java.util.Collections.singleton;

/**
 * 

Contains miscellaneous utility methods.

*/ public class StructureUtil { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(StructureUtil.class); public static final int MAX_PROPERTY_LENGTH = 250; public static final La STRING_LONG = new La() { @Override public Long la(String argument) { try { return argument == null ? null : Long.parseLong(argument.trim()); } catch (NumberFormatException e) { return null; } } }; public static final La INT_ITERATOR_TO_INT = new La() { @Override public Integer la(IntIterator intIterator) { return intIterator.value(); } }; public static final La LONG_ITERATOR_TO_LONG = new La() { @Override public Long la(LongIterator longIterator) { return longIterator.value(); } }; public static PermissionLevel applyPermissions(List permissions, ApplicationUser user, List stack, La> resolver, PermissionLevel pass) { PermissionLevel r = pass; if (permissions != null) { for (PermissionRule permission : permissions) { r = permission.apply(user, r, stack, resolver); } } return r; } public static JAXBContext createJAXBContext(Class contextClass) { Thread thread = Thread.currentThread(); ClassLoader contextLoader = thread.getContextClassLoader(); ClassLoader[] loaders = {ClassLoader.getSystemClassLoader(), contextLoader, JAXBContext.class.getClassLoader(), contextClass.getClassLoader()}; JAXBContext context = null; try { context = JAXBContext.newInstance(contextClass); } catch (JAXBException e) { for (int i = 1; i < loaders.length; i++) { try { thread.setContextClassLoader(loaders[i]); context = JAXBContext.newInstance(contextClass); if (context != null) break; } catch (JAXBException e2) { // ignore } } if (context == null) { logger.error("cannot initialize JAXB context for " + contextClass + " (tried loaders: " + asList(loaders) + ")", e); } } finally { thread.setContextClassLoader(contextLoader); } return context; } @Nullable public static Long getSingleParameterLong(@Nullable Map map, @Nullable String name) { if (map == null || name == null) return null; Object value = map.get(name); try { return getSingleLong0(value); } catch (NumberFormatException e) { logger.warn("bad parameter " + name + " [" + value + "]"); return null; } } @Nullable public static Integer getSingleParameterInteger(@Nullable Map map, @Nullable String name) { if (map == null || name == null) return null; Object value = map.get(name); try { Long l = getSingleLong0(value); return l == null ? null : Math.toIntExact(l); } catch (NumberFormatException | ArithmeticException e) { logger.warn("bad parameter " + name + " [" + value + "]"); return null; } } public static boolean getSingleParameterBoolean(@Nullable Map map, @Nullable String name) { if (map == null || name == null) return false; Object r = map.get(name); if (r instanceof Boolean) { return (Boolean) r; } String value = getSingleString(r); return value != null && "true".equalsIgnoreCase(value.trim()); } @Nullable public static String getSingleParameter(@Nullable Map map, @Nullable String name) { if (map == null || name == null) return null; return getSingleString(map.get(name)); } @Nullable public static String getSingleString(Object r) { if (r instanceof Collection) { Collection c = (Collection) r; r = c.isEmpty() ? null : c.iterator().next(); } else if (r instanceof String[]) { r = ((String[]) r).length == 0 ? null : ((String[]) r)[0]; } return r == null ? null : String.valueOf(r); } @Nullable public static Long getSingleLong(Object r) { try { return getSingleLong0(r); } catch (NumberFormatException e) { logger.warn("bad long value [" + r + "]"); return null; } } @Nullable private static Long getSingleLong0(Object r) { if (r instanceof Collection) { Collection c = (Collection) r; r = c.isEmpty() ? null : c.iterator().next(); } else if (r instanceof long[]) { r = ((long[]) r).length == 0 ? null : ((long[]) r)[0]; } else if (r instanceof Object[]) { r = ((Object[]) r).length == 0 ? null : ((Object[]) r)[0]; } if (r instanceof Number) { return ((Number) r).longValue(); } else if (r instanceof String) { String value = (String) r; if (!StringUtils.isBlank(value)) { try { return Long.parseLong(value); } catch (NumberFormatException nfe) { // suppress and try to parse double. sometimes it's an id string like 10000.0 return (long) Double.parseDouble(value); } } } return null; } @NotNull public static List getStringList(Object r) { if (r instanceof Collection) { Collection c = (Collection) r; ArrayList result = new ArrayList<>(c.size()); for (Object o : c) { if (o != null) result.add(String.valueOf(o)); } return result; } else if (r instanceof String[]) { String[] array = (String[]) r; return array.length == 0 ? Collections.emptyList() : asList(array); } return r == null ? Collections.emptyList() : Collections.singletonList(String.valueOf(r)); } @NotNull public static List getMultiParameter(@Nullable Map map, @Nullable String name) { if (map == null || name == null) return Collections.emptyList(); return getStringList(map.get(name)); } @NotNull public static List getMultiParameterLong(@Nullable Map map, @Nullable String name) { return STRING_LONG.arrayList(getMultiParameter(map, name)); } @NotNull public static List getCommaSeparatedParameter(@Nullable Map map, @Nullable String name, @NotNull Function converter) { String params = getSingleParameter(map, name); if (StringUtils.isBlank(params)) { return Collections.emptyList(); } return Arrays.stream(params.split(",")).map(converter).collect(Collectors.toList()); } /** * Sets a possibly long value (template) as an application property. Because properties may be limited to 255 * characters, this method breaks up long property into several properties each no longer than allowed. * * @param properties properties interface * @param name the name of the property * @param value the value */ public static void setLongProperty(ApplicationProperties properties, String name, String value) { if (value == null) value = ""; int len = value.length(); int chunk = Math.min(len, MAX_PROPERTY_LENGTH); properties.setString(name, value.substring(0, chunk)); value = value.substring(chunk); len -= chunk; int step = 1; while (len > 0) { chunk = Math.min(len, MAX_PROPERTY_LENGTH); properties.setString(name + "." + (step++), value.substring(0, chunk)); value = value.substring(chunk); len -= chunk; } while (true) { String n = name + "." + (step++); String s = properties.getDefaultBackedString(n); if (s == null || s.length() <= 0) { break; } properties.setString(n, null); } } /** * Reads a value previously stored in properties using setLongValue * * @param properties application propeties * @param name the name of the property to read * @return property value or an empty string */ @NotNull public static String getLongProperty(ApplicationProperties properties, String name) { StringBuilder r = new StringBuilder(); String s = properties.getDefaultBackedString(name); int step = 1; while (s != null && s.length() > 0) { r.append(s); s = properties.getDefaultBackedString(name + "." + (step++)); } return r.toString(); } /** * Similar to {@link Boolean#getBoolean(String)}, but with ability to specify default value if no value is specified. */ public static boolean getBooleanSystemProperty(String key, boolean defaultValue) { boolean result = defaultValue; try { String propertyValue = System.getProperty(key); if (propertyValue != null) { result = Boolean.parseBoolean(propertyValue); } } catch (IllegalArgumentException | NullPointerException e) { } return result; } /** * Returns the last element of the given list if it's not empty, 0 otherwise. * * @param list the list of longs * @return last value of list or 0 if it's empty. */ public static long lastOrZero(LongList list) { return (list == null || list.isEmpty()) ? 0 : list.get(list.size() - 1); } /** * Tests whether the given lists are equal (contain the same numbers in the same order). * An empty list is considered equal to a null list. * * @param list1 the first list * @param list2 the second list * @return true if the lists are equal, false otherwise. */ public static boolean listsEqual(LongList list1, LongList list2) { boolean empty1 = list1 == null || list1.isEmpty(); boolean empty2 = list2 == null || list2.isEmpty(); if (empty1 && empty2) return true; if (empty1 || empty2) return false; return list1.equals(list2); } @Nullable public static List copyPermissionsOrNull(@Nullable Collection permissions) { return permissions == null ? null : copyPermissions(permissions); } @NotNull public static List copyPermissions(@Nullable Collection permissions) { ArrayList r = new ArrayList<>(permissions == null ? 0 : permissions.size()); if (permissions != null) { for (PermissionRule permission : permissions) { r.add(permission.clone()); } } return r; } /** * Returns a string representation of the issue that is used to write log messages. Writes issue ID and, if possible, * issue key. * * @param issue the ID of the issue * @return string that can be used in output */ @NotNull public static String getDebugIssueString(@Nullable Long issue) { return issue == null ? "null" : appendDebugIssueString(issue, new StringBuilder()).toString(); } public static StringBuilder appendDebugIssueString(@Nullable Long issue, StringBuilder sb) { if (issue == null) sb.append("null"); else appendDebugIssueString(issue, getDebugIssueKey(issue), sb); return sb; } /** * Returns issue key if possible. Use for debug output. */ @Nullable public static String getDebugIssueKey(long issueId) { try { IssueManager issueManager = JiraComponents.getIssueManager(); MutableIssue io = issueManager == null ? null : issueManager.getIssueObject(issueId); return io == null ? null : io.getKey(); } catch (Exception | LinkageError ignored) { } return null; } /** * Returns a string representation of the issue that is used to write log messages. Writes issue ID and, if possible, * issue key. * * @param issue the issue * @return string that can be used in output */ @NotNull public static String getDebugIssueString(@Nullable Issue issue) { return issue == null ? "null" : appendDebugIssueString(issue.getId(), issue.getKey(), new StringBuilder()).toString(); } /** * @return issue key if the issue object is not {@code null}. Otherwise, if issue ID is not {@code null}, returns issue ID in the form "issue #123". Otherwise, * returns string "(no issue ID)". * Better suited for high-level log messages than {@link #getDebugIssueString(java.lang.Long)} in that it is easily recognizable as issue representation to JIRA admin. */ public static String issueKeyOrId(@Nullable Issue issue, @Nullable Long issueId) { return issue != null ? issue.getKey() : issueId != null ? "issue #" + issueId : "(no issue ID)"; } @NotNull public static StringBuilder appendDebugIssueString(@Nullable Long issueId, @Nullable String issueKey, StringBuilder r) { if (issueKey != null) { r.append('(').append(issueKey).append(' ').append(issueId).append(')'); } else { r.append(issueId); } return r; } @NotNull public static StringBuilder appendDebugStructureString(final long structureId, final StructureManager manager, final StringBuilder sb) { return StructureAuth.sudo(new SimpleCallable() { @Override public StringBuilder call() { try { Structure structure = manager.getStructure(structureId, PermissionLevel.NONE); String name = structure.getName(); return sb.append('\'').append(name).append("' (#").append(structureId).append(')'); } catch (StructureException ignored) { return sb.append("#").append(structureId); } } }); } public static String getDebugStructureString(long structureId, StructureManager structureManager) { return appendDebugStructureString(structureId, structureManager, new StringBuilder()).toString(); } @NotNull public static String username(@Nullable ApplicationUser user) {//j6 ok String username = user != null ? user.getName() : "anonymous"; //j6 ok return username == null ? "user[name = null]" : username; } public static String debugConstant(IssueConstant constant) { return constant == null ? "" : constant.getName() + "(" + constant.getId() + ")"; } public static String debugAction(ActionDescriptor action) { return action == null ? "" : action.getName() + "(" + action.getId() + ")"; } @NotNull public static String getTextInCurrentUserLocale(String key, Object... parameters) { return JiraI18n.forCurrentUser().getText(key, parameters); } @NotNull public static String getTextInCurrentUserLocale(@NotNull I18nText i18nText) { return getTextInCurrentUserLocale(i18nText.getI18nKey(), i18nText.getArguments()); } /** * Formats the text taken from the i18n bundle with the specified parameters in the locale that is selected as follows: *
    *
  1. If locale is not null, it is used.
  2. *
  3. Otherwise, if user is not null, the user locale (set up in the user preferences) is used.
  4. *
  5. If both locale and user are null, JIRA default locale is used. In a highly unlikely * event when JIRA application parameters are inaccessible, {@link java.util.Locale#ROOT root} (neutral) locale is used.
  6. *
* * @param messageParameters can be the same as {@link I18nHelper#getText(String, Object)} */ @NotNull public static String getText(@Nullable final Locale locale, @Nullable final ApplicationUser user, final String textKey, final Object... messageParameters) { return JiraI18n.create(locale, user).getText(textKey, messageParameters); } @NotNull public static Locale getJiraDefaultLocale() { ApplicationProperties appProps = JiraComponents.getApplicationProperties(); if (appProps == null) { logger.warn("No application properties: probably a System 2 plugin now? Using root (neutral) locale."); return Locale.ROOT; } Locale defaultLocale = appProps.getDefaultLocale(); if (defaultLocale == null) { logger.warn("No JIRA default locale. Using root (neutral) locale."); return Locale.ROOT; } return defaultLocale; } @Nullable public static String getUserKey(@Nullable ApplicationUser user) { return user == null ? null : user.getKey(); } @Nullable public static String getUserName(@Nullable ApplicationUser user) { return user == null ? null : user.getUsername(); } @Nullable public static ApplicationUser getUserByKey(@Nullable String userKey) { return JiraFunc.USERKEY_APPLICATIONUSER.la(userKey); } @Nullable public static ApplicationUser getApplicationUserByName(@Nullable String userName) { return JiraComponents.getUserManager().getUserByName(userName); } @Nullable public static String getUserNameByKey(@Nullable String userKey) { ApplicationUser user = getUserByKey(userKey); return user == null ? null : user.getName(); } @Nullable public static String migrateUserNameToUserKey(@Nullable String userName) { return userName == null ? null : IdentifierUtils.toLowerCase(userName); } /** * @return a fairly random number that is used to identify the scope of some other data for external components * with longer lifecycle (such as browsers or other addons). The signature is retrieved once per component * lifecycle and all other data it provides is valid only within the same signature. */ public static int createRuntimeSignature() { // todo for easier debugging and ensuring that the IDs are unique within the system, allocate a base number and then increment // static context is ok because the plugin is reloaded with the class long v = System.currentTimeMillis() + 1299827L * System.nanoTime() + 31 * new Object().hashCode(); return new Long(v).hashCode(); } public static String getDebugRowString(long row) { // todo return "(row " + row + ")"; } public static String getDebugRowString(StructureRow row) { // todo return row == null ? "" : row.toString(); } @NotNull public static String toJson(@Nullable Object object) { return JsonUtil.toJson(object); } @NotNull public static String toJson(@Nullable Object object, ObjectMapper mapper) { return JsonUtil.toJson(object, mapper); } @Nullable public static T fromJson(String specJson, Class expectedClass) { return JsonUtil.fromJson(specJson, expectedClass); } @Nullable public static T fromJson(String specJson, Class expectedClass, ObjectMapper mapper) { return JsonUtil.fromJson(specJson, expectedClass, mapper); } @Nullable public static T fromJson(String json, TypeReference typeRef) { return JsonUtil.fromJson(json, typeRef); } @Nullable public static T fromJson(String json, TypeReference typeRef, ObjectMapper mapper) { return JsonUtil.fromJson(json, typeRef, mapper); } @Nullable public static Map fromJson(String json) { return JsonUtil.fromJson(json); } @Nullable public static Map fromJson(String json, ObjectMapper mapper) { return JsonUtil.fromJson(json, mapper); } @Nullable public static T fromMap(Map json, Class expectedClass) { return fromMap(json, expectedClass, defaultMapper()); } @Nullable public static T fromMap(Map json, Class expectedClass, ObjectMapper mapper) { return JsonUtil.fromMap(json, expectedClass, mapper); } @Nullable public static Map toMap(Object object) { return JsonUtil.toMap(object); } @Nullable public static Map toMap(Object object, ObjectMapper mapper) { return JsonUtil.toMap(object, mapper); } public static ObjectMapper defaultMapper() { return JsonUtil.defaultMapper(); } public static ObjectMapper withUnknownPropertiesMapper() { return JsonUtil.withUnknownPropertiesMapper(); } public static Class> setType() { return TypeUtils.setType(); } public static Class> mapType() { return TypeUtils.mapType(); } public static Class> listType() { return TypeUtils.listType(); } public static boolean onlyOneIsTrue(boolean... booleans) { boolean result = false; for (boolean b : booleans) { if (b) { if (result) { result = false; break; } else { result = true; } } } return result; } public static void swap(List list, int a, int b) { T tmp = list.get(a); list.set(a, list.get(b)); list.set(b, tmp); } public static void warnExceptionIfDebug(Logger log, String message, Throwable t) { LoggingUtils.warnExceptionIfDebug(log, message, t); } public static String getMessageWithCauseChain(Throwable t) { return LoggingUtils.getMessageWithCauseChain(t); } @Nullable public static String getItemDescription(ItemIdentity itemId) { try { StructureAttributeService attributeService = JiraComponents.getOSGiComponentInstanceOfType(StructureAttributeService.class); if (attributeService != null) { ItemDisplayable displayable = getItemDisplayable(itemId, attributeService); return displayable.getDescription(); } } catch (Exception | LinkageError ignored) { } return null; } @NotNull public static ItemDisplayable getItemDisplayable(@NotNull ItemIdentity itemId, StructureAttributeService attributeService) { ItemDisplayable result = getItemDisplayable(singleton(itemId), attributeService).get(itemId); assert result != null; return result; } @NotNull public static ItemDisplayable getItemDisplayable(@NotNull StructureRow row, StructureAttributeService attributeService) { return getItemDisplayable(row.getItemId(), attributeService); } @NotNull public static Map getItemDisplayable(@NotNull Collection itemIds, StructureAttributeService attributeService) { ItemValues values = attributeService.getItemValues(itemIds, asList(CoreAttributeSpecs.DISPLAYABLE_TEXT, CoreAttributeSpecs.URL)); Map result = new HashMap<>(); for (ItemIdentity itemId : itemIds) { String description = values.get(itemId, CoreAttributeSpecs.DISPLAYABLE_TEXT); if (StringUtils.isEmpty(description)) { description = itemId.toString(); } String url = values.get(itemId, CoreAttributeSpecs.URL); result.put(itemId, new ItemDisplayable(description, url)); } return result; } @Nullable public static String getGroupName(Group group) { return group == null ? null : group.getName(); } public static boolean isMutuallyExclusive(LongList list1, LongList list2) { return IntegersUtil.isMutuallyExclusive(list1, list2); } public static boolean isMutuallyExclusive(LongSet set1, LongSet set2) { return IntegersUtil.isMutuallyExclusive(set1, set2); } @NotNull public static String toDebugJson(@Nullable Object object) { if (object == null) return ""; if (object instanceof String) return (String) object; ObjectMapper mapper = new ObjectMapper(); try { return mapper.writeValueAsString(object); } catch (IOException | OutOfMemoryError e) { // out of memory was seen when trying to serialize a VERY large response in a stress test return "?" + e + "?"; } } public static long nn(@Nullable Long value, long defaultValue) { return TypeUtils.nn(value, defaultValue); } public static int nn(@Nullable Integer value, int defaultValue) { return TypeUtils.nn(value, defaultValue); } @NotNull public static T nnv(@Nullable T value, @NotNull T defaultValue) { return TypeUtils.nnv(value, defaultValue); } public static long lv(String v, long defaultValue) { return TypeUtils.lv(v, defaultValue); } public static int iv(String v, int defaultValue) { return TypeUtils.iv(v, defaultValue); } public static Long lvn(String v) { return TypeUtils.lvn(v); } public static Integer ivn(String v) { return TypeUtils.ivn(v); } public static String nn(String value) { return TypeUtils.nn(value); } public static long nnl(Long value) { return TypeUtils.nnl(value); } public static int nni(Integer value) { return TypeUtils.nni(value); } @NotNull public static String getBaseUrl() { com.atlassian.sal.api.ApplicationProperties ap = JiraComponents.getOSGiComponentInstanceOfType( com.atlassian.sal.api.ApplicationProperties.class); if (ap != null) { return nn(ap.getBaseUrl()); } else { logger.warn("sal ApplicationProperties is null"); return ""; } } public static String decodeURL(String url) { try { return url == null ? "" : URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { logger.error("NO UTF-8?", e); return url; } catch (IllegalArgumentException e) { logger.error("when decoding [" + url + "]", e); return url; } } public static String encodeURL(String url) { try { return url == null ? "" : URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { logger.error("NO UTF-8?", e); return url; } catch (IllegalArgumentException e) { logger.error("when encoding [" + url + "]", e); return url; } } public static String encodeURIComponent(String cmp) { return encodeURL(cmp) .replaceAll("\\+", "%20") .replaceAll("%21", "!") .replaceAll("%27", "'") .replaceAll("%28", "(") .replaceAll("%29", ")") .replaceAll("%7E", "~"); } public static boolean isSubMap(Map map, Map subMap) { return MapUtils.isSubMap(map, subMap); } @Internal public static boolean isDevMode() { return "true".equals(System.getProperty("jira.dev.mode")) || "true".equals(System.getProperty("atlassian.dev.mode")); } @NotNull public static String nonBlank(@Nullable String value) throws IllegalArgumentException { if (StringUtils.isBlank(value)) { throw new IllegalArgumentException("argument cannot be null or blank"); } return value.trim(); } /** * Not thread safe */ public static Supplier memoize(Supplier supplier) { if (supplier instanceof MemoizingSupplier) return supplier; return new MemoizingSupplier<>(supplier); } private static class MemoizingSupplier implements Supplier { private Supplier mySupplier; private T myValue; private boolean myInitialized; public MemoizingSupplier(Supplier supplier) { mySupplier = supplier; } @Override public T get() { if (!myInitialized) { myInitialized = true; Supplier supplier = mySupplier; mySupplier = null; // no need to hold it in memory myValue = supplier.get(); } return myValue; } } }