PageRenderTime 57ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 1ms

/src/main/groovy/com/onresolve/jira/groovy/GroovyCustomField.groovy

https://bitbucket.org/sorin/scriptrunner-public-sorin
Groovy | 415 lines | 282 code | 68 blank | 65 comment | 36 complexity | d49b446fa61b399e0d49ea5eedc43cab MD5 | raw file
  1. package com.onresolve.jira.groovy;
  2. import com.atlassian.jira.ComponentManager
  3. import com.atlassian.jira.ManagerFactory
  4. import com.atlassian.jira.component.ComponentAccessor
  5. import com.atlassian.jira.config.properties.ApplicationProperties
  6. import com.atlassian.jira.datetime.DateTimeFormatterFactory
  7. import com.atlassian.jira.datetime.DateTimeStyle
  8. import com.atlassian.jira.issue.Issue
  9. import com.atlassian.jira.issue.customfields.SortableCustomField
  10. import com.atlassian.jira.issue.customfields.converters.StringConverter
  11. import com.atlassian.jira.issue.customfields.impl.CalculatedCFType
  12. import com.atlassian.jira.issue.customfields.impl.FieldValidationException
  13. import com.atlassian.jira.issue.fields.CustomField
  14. import com.atlassian.jira.issue.fields.config.FieldConfig
  15. import com.atlassian.jira.issue.fields.config.manager.FieldConfigSchemeManager
  16. import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem
  17. import com.atlassian.jira.security.JiraAuthenticationContext
  18. import com.atlassian.jira.security.PermissionManager
  19. import com.atlassian.jira.security.Permissions
  20. import com.atlassian.jira.util.DateFieldFormat
  21. import com.atlassian.jira.util.JiraVelocityUtils
  22. import com.atlassian.jira.util.velocity.NumberTool
  23. import com.atlassian.velocity.VelocityManager
  24. import com.onresolve.jira.groovy.customfield.CustomFieldConfiguration
  25. import java.security.MessageDigest
  26. import javax.script.Bindings
  27. import javax.script.ScriptEngine
  28. import javax.script.SimpleBindings
  29. import org.apache.log4j.Category
  30. import com.atlassian.jira.config.properties.APKeys
  31. import com.atlassian.jira.issue.fields.config.FieldConfigScheme
  32. /**
  33. * User: echlinj
  34. * Date: 04-Apr-2008
  35. * Time: 17:29:41
  36. */
  37. public class GroovyCustomField extends CalculatedCFType implements SortableCustomField {
  38. private final StringConverter stringConverter;
  39. private Category log = Category.getInstance(GroovyCustomField.class);
  40. private final ApplicationProperties applicationProperties;
  41. private String script;
  42. private String scriptFile;
  43. private String modelTemplate;
  44. private String customTemplate;
  45. private String exceptionMessage;
  46. private final PermissionManager permissionManager;
  47. private final JiraAuthenticationContext authenticationContext;
  48. public boolean previewMode = false
  49. ScriptManager scriptManager
  50. public GroovyCustomField(StringConverter stringConverter,
  51. ApplicationProperties applicationProperties,
  52. final PermissionManager permissionManager, final JiraAuthenticationContext authenticationContext,
  53. final ScriptManager scriptManager)
  54. {
  55. super();
  56. this.stringConverter = stringConverter;
  57. this.applicationProperties = applicationProperties;
  58. this.permissionManager = permissionManager;
  59. this.authenticationContext = authenticationContext;
  60. this.scriptManager = scriptManager
  61. }
  62. public Object getCustomFieldValue() {
  63. return "bazooka";
  64. }
  65. // @Override
  66. // public List<FieldIndexer> getRelatedIndexers(CustomField customField) {
  67. // // seems to only be called when the cache is refreshed.
  68. // log.warn("GroovyCustomField.getRelatedIndexers");
  69. // return EasyList.build(new MultiGroupCustomFieldIndexer(fieldVisibilityManager, customField, multiGroupConverter));
  70. // }
  71. /*
  72. * This is what is called by the indexer, but only if an indexer is setup for this CF.
  73. * Also called 3 times per issue, hence caching
  74. */
  75. public Object getValueFromIssue(CustomField customField, Issue issue) {
  76. // this is the only place the groovy should happen
  77. // run velocity template
  78. setExceptionMessage(null);
  79. String fieldIssueId = getFieldIssueId(customField, issue);
  80. if (result.get().containsKey(fieldIssueId)) {
  81. // if (log.isDebugEnabled()) {
  82. // log.debug("avoided script eval for issue: " + issue.getKey() + " cf: " + customField.getName());
  83. // }
  84. return result.get().get(fieldIssueId);
  85. }
  86. // scriptManager.setupConfig();
  87. CustomFieldConfiguration config = getRelevantConfig(customField, issue);
  88. // preview mode the script will be set
  89. config = setConfigForPreview(config)
  90. if (config == null || (! config.getScript() && ! config.getScriptFile())) {
  91. // no idea why this is called when it does not apply
  92. // if (log.isDebugEnabled()) {
  93. // log.debug("Field not set up correctly, or this field does not apply to this issue: " + issue.getKey());
  94. // }
  95. return null;
  96. }
  97. Bindings bindings = new SimpleBindings();
  98. def customFieldManager = ComponentAccessor.getCustomFieldManager()
  99. def getCustomFieldValue = {Object cfId ->
  100. def customFields = customFieldManager.getCustomFieldObjects(issue)
  101. def cf
  102. if (cfId instanceof String) {
  103. cf = customFields.find {it.name.equals(cfId)}
  104. }
  105. else {
  106. cf = customFields.find {it.idAsLong == cfId}
  107. }
  108. if (!cf) {
  109. log.error("getCustomFieldValue called from script: No field with ID or name $cfId")
  110. return null
  111. }
  112. return issue.getCustomFieldValue(cf)
  113. }
  114. bindings.put("issue", issue);
  115. bindings.put("log", log);
  116. bindings.put("componentManager", ComponentManager.getInstance());
  117. bindings.put("getCustomFieldValue", getCustomFieldValue);
  118. // use a base class here...?
  119. ScriptEngine gse = CannedScriptRunner.getGse();
  120. try {
  121. Object eval;
  122. def scriptFile = config.getScriptFile()
  123. if (scriptFile) {
  124. String scriptText = getTextFromFileOrUrl(scriptFile)
  125. eval = gse.eval(scriptText, bindings);
  126. }
  127. else {
  128. eval = gse.eval(config.getScript(), bindings);
  129. // for (int i = 0; i < 10; i++) {
  130. // log.debug("parse: " + i);
  131. // gse.eval(config.getScript(), bindings);
  132. // }
  133. }
  134. // would like to use GroovyShell here to set a base class for the script but it's much slower than script engine,
  135. // unless we implement the caching ourselves as in behaviours
  136. if (eval != null) {
  137. if (log.isDebugEnabled()) {
  138. log.debug("Script was executed for cf: " + customField.getName() +
  139. " and issue " + issue.getKey() +
  140. " and returned: " + eval.toString());
  141. }
  142. }
  143. // don't cache result when run through the previewer
  144. if (! getPreviewMode()) {
  145. result.get().put(fieldIssueId, eval);
  146. }
  147. return eval;
  148. } catch (Exception e) {
  149. log.error(e.getMessage(), e);
  150. setExceptionMessage(e.getMessage());
  151. }
  152. return null;
  153. }
  154. private getTextFromFileOrUrl(String scriptFile) {
  155. String scriptText
  156. try {
  157. scriptText = new URL(scriptFile).text
  158. }
  159. catch (MalformedURLException e) {
  160. FileInputStream fileInputStream = new FileInputStream(scriptFile);
  161. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
  162. scriptText = bufferedReader.text
  163. }
  164. scriptText
  165. }
  166. private setConfigForPreview(CustomFieldConfiguration config) {
  167. if (getPreviewMode()) {
  168. // if we don't have a configuration then it's because this issue is not in the context of this custom field
  169. // which is fine for previewing, just use a dummy one
  170. if (!config) {
  171. config = new CustomFieldConfiguration()
  172. }
  173. config.setScript(script);
  174. config.setScriptFile(scriptFile)
  175. config.setModelTemplate(modelTemplate)
  176. config.setCustomTemplate(customTemplate)
  177. }
  178. return config
  179. }
  180. private String getFieldIssueId(CustomField customField, Issue issue) {
  181. if (issue != null && issue.getId() != null && customField != null) {
  182. // some measure of caching. At worst this is going to cause the script to run more often than we'd like, not less
  183. // return issue.getId() << 8 + customField.getIdAsLong() << 16 + (long) (System.currentTimeMillis() / 1000);
  184. // better way but may be slower:
  185. MessageDigest digest = MessageDigest.getInstance("MD5")
  186. digest.update((issue.id.toString() + customField.id + ((long) (System.currentTimeMillis() / 1000)).toString()).bytes)
  187. BigInteger big = new BigInteger(1,digest.digest())
  188. String md5 = big.toString(16).padLeft(32,"0")
  189. return md5
  190. }
  191. else {
  192. return null;
  193. }
  194. }
  195. private CustomFieldConfiguration getRelevantConfig(CustomField customField, Issue issue) {
  196. FieldConfigSchemeManager fieldConfigSchemeManager = ComponentManager.getComponentInstanceOfType(FieldConfigSchemeManager.class);
  197. FieldConfigScheme relevantConfig = fieldConfigSchemeManager.getRelevantConfigScheme(issue, customField);
  198. if (relevantConfig == null) {
  199. return null;
  200. }
  201. // def scriptManager = ComponentManager.getComponentInstanceOfType(ScriptManager.class)
  202. CustomFieldConfiguration configFor = scriptManager.getConfigFor(relevantConfig.getId());
  203. return configFor;
  204. }
  205. public String getStringFromSingularObject(Object value)
  206. {
  207. log.debug("getStringFromSingularObject called with value: " + value);
  208. // todo: check the right type or subvert?
  209. // assertObjectImplementsType(String.class, value);
  210. // return stringConverter.getString((String) value);
  211. // return string rep of what was produced by getvaluefromissue
  212. return value.toString();
  213. }
  214. /**
  215. * Called when updating
  216. */
  217. public Object getSingularObjectFromString(String string) throws FieldValidationException
  218. {
  219. // log.debug("getSingularObjectFromString: string: " + string);
  220. // could be number type
  221. return stringConverter.getObject(string);
  222. }
  223. @Override
  224. @SuppressWarnings("unchecked")
  225. public Map getVelocityParameters(Issue issue, CustomField field, FieldLayoutItem fieldLayoutItem) {
  226. Map map = super.getVelocityParameters(issue, field, fieldLayoutItem);
  227. map.put("issue", issue);
  228. map.put("field", field);
  229. map.put("fieldLayoutItem", fieldLayoutItem);
  230. map.put("groovyCustomField", this);
  231. return map;
  232. }
  233. @SuppressWarnings("unchecked")
  234. public Map getRealVelocityParameters(Issue issue, CustomField field, FieldLayoutItem fieldLayoutItem) {
  235. String fieldIssueId = getFieldIssueId(field, issue);
  236. // log.debug("fieldIssueId: " + fieldIssueId);
  237. // log.debug("threadlocal result: " + result.get().get(fieldIssueId));
  238. // Map map = super.getVelocityParameters(issue, field, fieldLayoutItem);
  239. Map<String, Object> map = JiraVelocityUtils.createVelocityParams(ComponentManager.getInstance().getJiraAuthenticationContext());
  240. map.put("groovyCustomField", this);
  241. map.put("issue", issue);
  242. map.put("customField", field);
  243. map.put("field", field);
  244. map.put("fieldLayoutItem", fieldLayoutItem);
  245. map.put("value", getValueFromIssue(field, issue));
  246. // map.put ("velocityParams", map);
  247. map.put("numberTool", new NumberTool(getI18nBean().getLocale()));
  248. map.put("dateFieldFormat", ComponentAccessor.getComponentOfType(DateFieldFormat.class));
  249. DateTimeFormatterFactory dateTimeFormatterFactory = ComponentAccessor.getComponentOfType(DateTimeFormatterFactory.class);
  250. map.put("iso8601Formatter", dateTimeFormatterFactory.formatter().withStyle(DateTimeStyle.ISO_8601_DATE).withSystemZone());
  251. map.put("datePickerFormatter", dateTimeFormatterFactory.formatter().forLoggedInUser().withStyle(DateTimeStyle.DATE_TIME_PICKER));
  252. map.put("titleFormatter", dateTimeFormatterFactory.formatter().withStyle(DateTimeStyle.COMPLETE));
  253. map.put("hasAdminPermission", permissionManager.hasPermission(Permissions.ADMINISTER, authenticationContext.getUser()));
  254. // not right for 4.4
  255. //map.put("userformat", userFormatManager.getUserFormat(ProfileLinkUserFormat.TYPE));
  256. // not sure, need to test dates
  257. // map.put("outlookdate", ComponentManager.getInstance().getJiraAuthenticationContext().getOutlookDate());
  258. // result.get().remove(fieldIssueId);
  259. return map;
  260. }
  261. public String getHtml(Map startingParams)
  262. {
  263. try
  264. {
  265. CustomField customField = (CustomField) startingParams.get("field");
  266. Issue issue = (Issue) startingParams.get("issue");
  267. CustomFieldConfiguration config = getRelevantConfig(customField, issue);
  268. // preview mode the script will be set
  269. config = setConfigForPreview(config)
  270. if (config == null) {
  271. return "Field not set up correctly";
  272. }
  273. if (! (config.getScript() || config.getScriptFile())) {
  274. return "Field not set up correctly - script is not defined";
  275. }
  276. String viewTemplate = config.getViewTemplate();
  277. def customTemplate = config.getCustomTemplate()
  278. if (!viewTemplate && !customTemplate) {
  279. return "Field not set up correctly - view template is not defined";
  280. }
  281. VelocityManager velocityManager = ManagerFactory.getVelocityManager();
  282. String encodedBody
  283. if (config.getModelTemplate() == "custom") {
  284. encodedBody = velocityManager.getEncodedBodyForContent (customTemplate, applicationProperties.getString(APKeys.JIRA_BASEURL), startingParams);
  285. }
  286. else {
  287. encodedBody = velocityManager.getEncodedBody("", viewTemplate, applicationProperties.getEncoding(), startingParams);
  288. }
  289. if (log.isDebugEnabled()) {
  290. log.debug("Encoded body: " + encodedBody);
  291. }
  292. return encodedBody;
  293. }
  294. catch (Throwable e)
  295. {
  296. log.error("Error while rendering velocity template for ....", e);
  297. }
  298. return "Error in getting html";
  299. }
  300. public String doVelocityMerge(Map params) {
  301. return getHtml(params);
  302. }
  303. public int compare(Object customFieldObjectValue1, Object customFieldObjectValue2, FieldConfig fieldConfig)
  304. {
  305. return ((String) customFieldObjectValue1).compareTo((String) customFieldObjectValue2);
  306. }
  307. public void setScript(String script) {
  308. this.script = script;
  309. }
  310. public void setScriptFile(String scriptFile) {
  311. this.scriptFile = scriptFile;
  312. }
  313. public void setModelTemplate(String modelTemplate) {
  314. this.modelTemplate = modelTemplate;
  315. }
  316. public void setCustomTemplate(String customTemplate) {
  317. this.customTemplate = customTemplate;
  318. }
  319. public String getExceptionMessage() {
  320. return exceptionMessage;
  321. }
  322. public void setExceptionMessage(String exceptionMessage) {
  323. this.exceptionMessage = exceptionMessage;
  324. }
  325. private String getBaseUrl()
  326. {
  327. // final ApplicationProperties applicationProperties = ComponentManager.getInstance().getApplicationProperties();
  328. // final VelocityRequestContext velocityRequestContext = new VelocityRequestContextFactory(applicationProperties).getJiraVelocityRequestContext();
  329. // return velocityRequestContext.getBaseUrl();
  330. return "";
  331. }
  332. /**
  333. * Needs to be a map as the indexer will otherwise get the same value for all issues
  334. */
  335. private static ThreadLocal<Map<String,Object>> result = new ThreadLocal<Map<String,Object>>() {
  336. @Override
  337. protected Map<String,Object> initialValue() {
  338. return [:];
  339. }
  340. };
  341. public void setPreviewMode(boolean previewMode) {
  342. this.previewMode = previewMode
  343. }
  344. public boolean getPreviewMode() {
  345. return previewMode
  346. }
  347. }