Commits

Steve Hetland  committed 7145e56

Updated to JIRA 6.0.4

  • Participants
  • Parent commits ac6cfb9

Comments (0)

Files changed (32)

-To avoid future confusion, we recommend that you include a license with your plugin.
-This file is simply a reminder.
+Copyright 2013 Atlassian Pty Ltd
 
-For a template license you can have a look at: http://www.opensource.org/licenses/
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
 
-Atlassian releases most of its modules under a BSD license: http://www.opensource.org/licenses/bsd-license.php
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
 
 Full documentation is always available at:
 
-https://developer.atlassian.com/display/JIRADEV/Plugin+Tutorial+-+Creating+a+JIRA+Report
+https://developer.atlassian.com/display/JIRADEV/Creating+a+JIRA+Reports
 <?xml version="1.0" encoding="UTF-8"?>
 
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         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">
-
+<project xmlns="http://maven.apache.org/POM/4.0.0" 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">
     <modelVersion>4.0.0</modelVersion>
-    <groupId>com.atlassian.plugins.tutorial</groupId>
+    <groupId>com.atlassian.plugins.tutorial.jira</groupId>
     <artifactId>jira-report-plugin</artifactId>
     <version>1.0-SNAPSHOT</version>
-
     <organization>
         <name>Example Company</name>
         <url>http://www.example.com/</url>
     </organization>
-
     <name>jira-report-plugin</name>
-    <description>This is the com.atlassian.tutorial:jira-report-plugin plugin for Atlassian JIRA.</description>
+    <description>Extends JIRA issue reports.</description>
     <packaging>atlassian-plugin</packaging>
-
     <dependencies>
         <dependency>
             <groupId>com.atlassian.jira</groupId>
-            <artifactId>atlassian-jira</artifactId>
+            <artifactId>jira-api</artifactId>
+            <version>${jira.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- Add dependency on jira-core if you want access to JIRA implementation classes as well as the sanctioned API.-->
+        <dependency>
+            <groupId>com.atlassian.jira</groupId>
+            <artifactId>jira-core</artifactId>
             <version>${jira.version}</version>
             <scope>provided</scope>
-        </dependency>        
+        </dependency>
+
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.6</version>
+            <version>4.10</version>
             <scope>test</scope>
         </dependency>
+        <!-- WIRED TEST RUNNER DEPENDENCIES -->
         <dependency>
-            <groupId>com.atlassian.jira</groupId>
-            <artifactId>jira-func-tests</artifactId>
-            <version>${jira.version}</version>
+            <groupId>com.atlassian.plugins</groupId>
+            <artifactId>atlassian-plugins-osgi-testrunner</artifactId>
+            <version>${plugin.testrunner.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>jsr311-api</artifactId>
+            <version>1.1.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.2.2-atlassian-1</version>
+        </dependency>
+        <!-- Uncomment to use TestKit in your project. Details at https://bitbucket.org/atlassian/jira-testkit -->
+        <!-- You can read more about TestKit at https://developer.atlassian.com/display/JIRADEV/Plugin+Tutorial+-+Smarter+integration+testing+with+TestKit -->
+        <!--
+		<dependency>
+			<groupId>com.atlassian.jira.tests</groupId>
+			<artifactId>jira-testkit-client</artifactId>
+			<version>${testkit.version}</version>
+			<scope>test</scope>
+		</dependency>
+		-->
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.8.5</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
-
     <build>
         <plugins>
             <plugin>
                 <groupId>com.atlassian.maven.plugins</groupId>
                 <artifactId>maven-jira-plugin</artifactId>
-                <version>3.0.5</version>
+                <version>${amps.version}</version>
                 <extensions>true</extensions>
                 <configuration>
                     <productVersion>${jira.version}</productVersion>
-                    <productDataVersion>${jira.data.version}</productDataVersion>
+                    <productDataVersion>${jira.version}</productDataVersion>
+                    <!-- Uncomment to install TestKit backdoor in JIRA. -->
+                    <!--
+					<pluginArtifacts>
+						<pluginArtifact>
+							<groupId>com.atlassian.jira.tests</groupId>
+							<artifactId>jira-testkit-plugin</artifactId>
+							<version>${testkit.version}</version>
+						</pluginArtifact>
+					</pluginArtifacts>
+					-->
                 </configuration>
             </plugin>
             <plugin>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
-                    <source>1.5</source>
-                    <target>1.5</target>
+                    <source>1.6</source>
+                    <target>1.6</target>
                 </configuration>
             </plugin>
         </plugins>
     </build>
-
     <properties>
-        <jira.version>4.1.1.1</jira.version>
-        <jira.data.version>4.1.1</jira.data.version>
+        <jira.version>6.0.4</jira.version>
+        <amps.version>4.2.0</amps.version>
+        <plugin.testrunner.version>1.1.1</plugin.testrunner.version>
+        <!-- TestKit version 5.x for JIRA 5.x, 6.x for JIRA 6.x -->
+        <testkit.version>5.2.26</testkit.version>
     </properties>
-
 </project>

File src/main/java/com/atlassian/plugins/tutorial/jira/MyPluginComponent.java

+package com.atlassian.plugins.tutorial.jira;
+
+public interface MyPluginComponent
+{
+    String getName();
+}

File src/main/java/com/atlassian/plugins/tutorial/jira/MyPluginComponentImpl.java

+package com.atlassian.plugins.tutorial.jira;
+
+import com.atlassian.sal.api.ApplicationProperties;
+
+public class MyPluginComponentImpl implements MyPluginComponent
+{
+    private final ApplicationProperties applicationProperties;
+
+    public MyPluginComponentImpl(ApplicationProperties applicationProperties)
+    {
+        this.applicationProperties = applicationProperties;
+    }
+
+    public String getName()
+    {
+        if(null != applicationProperties)
+        {
+            return "myComponent:" + applicationProperties.getDisplayName();
+        }
+        
+        return "myComponent";
+    }
+}

File src/main/java/com/atlassian/plugins/tutorial/jira/report/CreationReport.java

-package com.atlassian.plugins.tutorial.jira.report;
-
-import com.atlassian.core.util.DateUtils;
-import com.atlassian.jira.issue.search.SearchException;
-import com.atlassian.jira.issue.search.SearchProvider;
-import com.atlassian.jira.jql.builder.JqlQueryBuilder;
-import com.atlassian.jira.plugin.report.impl.AbstractReport;
-import com.atlassian.jira.project.ProjectManager;
-import com.atlassian.jira.util.I18nHelper;
-import com.atlassian.jira.util.ParameterUtils;
-import com.atlassian.jira.web.action.ProjectActionSupport;
-import com.atlassian.jira.web.bean.I18nBean;
-import com.atlassian.jira.web.util.OutlookDate;
-import com.atlassian.jira.web.util.OutlookDateManager;
-import com.atlassian.query.Query;
-import com.opensymphony.user.User;
-
-import org.apache.log4j.Logger;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Generate a histogram displaying number of issues opened in a specified period.
- * The time period is divided by the specifed value for the histogram display.
- */
-public class CreationReport extends AbstractReport
-{
-    private static final Logger log = Logger.getLogger(CreationReport.class);
-
-    // The max height for each bar in the histogram
-    private static final int MAX_HEIGHT = 200;
-    // Default interval value
-    private Long DEFAULT_INTERVAL = new Long(7);
-
-    // The highest issue count encountered in a search
-    private long maxCount = 0;
-    // A collection of issue open counts
-    private Collection<Long> openIssueCounts = new ArrayList<Long>();
-    // A collection of interval start dates - correlating with the openIssueCount collection.
-    private Collection<Date> dates = new ArrayList<Date>();
-
-    private final SearchProvider searchProvider;
-    private final OutlookDateManager outlookDateManager;
-    private final ProjectManager projectManager;
-
-    public CreationReport(SearchProvider searchProvider, OutlookDateManager outlookDateManager, ProjectManager projectManager)
-    {
-        this.searchProvider = searchProvider;
-        this.outlookDateManager = outlookDateManager;
-        this.projectManager = projectManager;
-    }
-
-    // Generate the report
-    public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception
-    {
-        User remoteUser = action.getRemoteUser();
-        I18nHelper i18nBean = new I18nBean(remoteUser);
-
-        // Retrieve the project parameter
-        Long projectId = ParameterUtils.getLongParam(params, "projectid");
-        // Retrieve the start and end dates and the time interval specified by the user
-        Date startDate = ParameterUtils.getDateParam(params, "startDate", i18nBean.getLocale());
-        Date endDate = ParameterUtils.getDateParam(params, "endDate", i18nBean.getLocale());
-        Long interval = ParameterUtils.getLongParam(params, "interval");
-
-        // Ensure that the interval is valid
-        if (interval == null || interval.longValue() <= 0)
-        {
-            interval = DEFAULT_INTERVAL;
-            log.error(action.getText("report.issuecreation.default.interval"));
-        }
-
-        getIssueCount(startDate, endDate, interval, remoteUser, projectId);
-
-        List<Number> normalCount = new ArrayList<Number>();
-
-        // Normalise the counts for the max height
-        if (maxCount != MAX_HEIGHT && maxCount > 0)
-        {
-            for (Long asLong : openIssueCounts)
-            {
-                Float floatValue = new Float((asLong.floatValue() / maxCount) * MAX_HEIGHT);
-                // Round it back to an integer
-                Integer newValue = new Integer(floatValue.intValue());
-
-                normalCount.add(newValue);
-            }
-        }
-
-        if (maxCount < 0)
-            action.addErrorMessage(action.getText("report.issuecreation.error"));
-
-        // Pass the issues to the velocity template
-        Map<String, Object> velocityParams = new HashMap<String, Object>();
-        velocityParams.put("startDate", startDate);
-        velocityParams.put("endDate", endDate);
-        velocityParams.put("openCount", openIssueCounts);
-        velocityParams.put("normalisedCount", normalCount);
-        velocityParams.put("dates", dates);
-        velocityParams.put("maxHeight", new Integer(MAX_HEIGHT));
-        velocityParams.put("outlookDate", outlookDateManager.getOutlookDate(i18nBean.getLocale()));
-        velocityParams.put("projectName", projectManager.getProjectObj(projectId).getName());
-        velocityParams.put("interval", interval);
-
-        return descriptor.getHtml("view", velocityParams);
-    }
-
-    // Retrieve the issues opened during the time period specified.
-    private long getOpenIssueCount(User remoteUser, Date startDate, Date endDate, Long projectId) throws SearchException
-    {
-        JqlQueryBuilder queryBuilder = JqlQueryBuilder.newBuilder();
-        Query query = queryBuilder.where().createdBetween(startDate, endDate).and().project(projectId).buildQuery();
-    
-        return searchProvider.searchCount(query, remoteUser);
-    }
-
-    private void getIssueCount(Date startDate, Date endDate, Long interval, User remoteUser, Long projectId) throws SearchException
-    {
-        // Calculate the interval value in milliseconds
-        long intervalValue = interval.longValue() * DateUtils.DAY_MILLIS;
-        Date newStartDate;
-        long count = 0;
-
-        // Split the specified time period by the interval value
-        while (startDate.before(endDate))
-        {
-            newStartDate = new Date(startDate .getTime() + intervalValue);
-
-            // Retrieve the issues opened within the time interval
-            if (newStartDate.after(endDate))
-                count = getOpenIssueCount(remoteUser, startDate, endDate, projectId);
-            else
-                count = getOpenIssueCount(remoteUser, startDate, newStartDate, projectId);
-
-            // Store the highest count for normalisation of results
-            if (maxCount < count)
-                maxCount = count;
-
-            // Store the count and the start date for this period
-            openIssueCounts.add(new Long(count));
-            dates.add(startDate);
-
-            // Move start date to next period
-            startDate = newStartDate;
-        }
-    }
-
-    // Validate the parameters set by the user.
-    public void validate(ProjectActionSupport action, Map params)
-    {
-        User remoteUser = action.getRemoteUser();
-        I18nHelper i18nBean = new I18nBean(remoteUser);
-
-        Date startDate = ParameterUtils.getDateParam(params, "startDate", i18nBean.getLocale());
-        Date endDate = ParameterUtils.getDateParam(params, "endDate", i18nBean.getLocale());
-        Long interval = ParameterUtils.getLongParam(params, "interval");
-        Long projectId = ParameterUtils.getLongParam(params, "projectid");
-
-        OutlookDate outlookDate = outlookDateManager.getOutlookDate(i18nBean.getLocale());
-
-        if (startDate == null || !outlookDate.isDatePickerDate(outlookDate.formatDMY(startDate)))
-            action.addError("startDate", action.getText("report.issuecreation.startdate.required"));
-
-        if (endDate == null || !outlookDate.isDatePickerDate(outlookDate.formatDMY(endDate)))
-            action.addError("endDate", action.getText("report.issuecreation.enddate.required"));
-
-        if (interval == null || interval.longValue() <= 0)
-            action.addError("interval", action.getText("report.issuecreation.interval.invalid"));
-
-        if (projectId == null)
-            action.addError("projectid", action.getText("report.issuecreation.projectid.invalid"));
-
-        // The end date must be after the start date
-        if (startDate != null && endDate != null && endDate.before(startDate))
-        {
-            action.addError("endDate", action.getText("report.issuecreation.before.startdate"));
-        }
-    }
-}

File src/main/java/com/atlassian/plugins/tutorial/jira/report/SingleLevelGroupByReportExtended.java

-package com.atlassian.plugins.tutorial.jira.report;
-
-import com.atlassian.core.util.map.EasyMap;
-import com.atlassian.jira.bc.JiraServiceContext;
-import com.atlassian.jira.bc.JiraServiceContextImpl;
-import com.atlassian.jira.bc.filter.SearchRequestService;
-import com.atlassian.jira.bc.issue.search.SearchService;
-import com.atlassian.jira.exception.PermissionException;
-import com.atlassian.jira.issue.CustomFieldManager;
-import com.atlassian.jira.issue.IssueFactory;
-import com.atlassian.jira.issue.index.IssueIndexManager;
-import com.atlassian.jira.issue.search.ReaderCache;
-import com.atlassian.jira.issue.search.SearchException;
-import com.atlassian.jira.issue.search.SearchProvider;
-import com.atlassian.jira.issue.search.SearchRequest;
-import com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator;
-import com.atlassian.jira.issue.statistics.StatisticsMapper;
-import com.atlassian.jira.issue.statistics.StatsGroup;
-import com.atlassian.jira.issue.statistics.util.OneDimensionalDocIssueHitCollector;
-import com.atlassian.jira.plugin.report.impl.AbstractReport;
-import com.atlassian.jira.security.JiraAuthenticationContext;
-import com.atlassian.jira.web.FieldVisibilityManager;
-import com.atlassian.jira.web.action.ProjectActionSupport;
-import com.atlassian.jira.web.bean.FieldVisibilityBean;
-import com.atlassian.jira.web.bean.PagerFilter;
-import com.atlassian.jira.web.util.OutlookDateManager;
-import com.atlassian.util.profiling.UtilTimerStack;
-import com.opensymphony.user.User;
-import com.opensymphony.util.TextUtils;
-
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.lucene.search.HitCollector;
-
-import java.util.Map;
-
-public class SingleLevelGroupByReportExtended extends AbstractReport
-{
-    private static Log log = LogFactory.getLog(SingleLevelGroupByReportExtended.class);
-
-    private final SearchProvider searchProvider;
-    private final JiraAuthenticationContext authenticationContext;
-    private final SearchRequestService searchRequestService;
-    private final IssueFactory issueFactory;
-    private final CustomFieldManager customFieldManager;
-    private final IssueIndexManager issueIndexManager;
-    private final SearchService searchService;
-    private final FieldVisibilityManager fieldVisibilityManager;
-    private final ReaderCache readerCache;
-    private final OutlookDateManager outlookDateManager;
-
-    public SingleLevelGroupByReportExtended(final SearchProvider searchProvider, final JiraAuthenticationContext authenticationContext,
-            final SearchRequestService searchRequestService, final IssueFactory issueFactory,
-            final CustomFieldManager customFieldManager, final IssueIndexManager issueIndexManager,
-            final SearchService searchService, final FieldVisibilityManager fieldVisibilityManager,
-            final ReaderCache readerCache, final OutlookDateManager outlookDateManager)
-    {
-        this.searchProvider = searchProvider;
-        this.authenticationContext = authenticationContext;
-        this.searchRequestService = searchRequestService;
-        this.issueFactory = issueFactory;
-        this.customFieldManager = customFieldManager;
-        this.issueIndexManager = issueIndexManager;
-        this.searchService = searchService;
-        this.fieldVisibilityManager = fieldVisibilityManager;
-        this.readerCache = readerCache;
-        this.outlookDateManager = outlookDateManager;
-    }
-
-    public StatsGroup getOptions(SearchRequest sr, User user, StatisticsMapper mapper) throws PermissionException
-    {
-
-        try
-        {
-            return searchMapIssueKeys(sr, user, mapper);
-        }
-        catch (SearchException e)
-        {
-            log.error("Exception rendering " + this.getClass().getName() + ".  Exception " + e.getMessage(), e);
-            return null;
-        }
-    }
-
-    public StatsGroup searchMapIssueKeys(SearchRequest request, User searcher, StatisticsMapper mapper)
-            throws SearchException
-    {
-        try
-        {
-            UtilTimerStack.push("Search Count Map");
-            StatsGroup statsGroup = new StatsGroup(mapper);
-            HitCollector hitCollector = new OneDimensionalDocIssueHitCollector(mapper.getDocumentConstant(), statsGroup, 
-                    issueIndexManager.getIssueSearcher().getIndexReader(), issueFactory,
-                    fieldVisibilityManager, readerCache);
-            searchProvider.searchAndSort((request != null) ? request.getQuery() : null, searcher, hitCollector, PagerFilter.getUnlimitedFilter());
-            return statsGroup;
-        }
-        finally
-        {
-            UtilTimerStack.pop("Search Count Map");
-        }
-    }
-
-    public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception
-    {
-        String filterId = (String) params.get("filterid");
-        if (filterId == null)
-        {
-            log.error("Single Level Group By Report run without a project selected (JRA-5042): params=" + params);
-            return "<span class='errMsg'>No search filter has been selected. Please "
-                   + "<a href=\"IssueNavigator.jspa?reset=Update&amp;pid="
-                   + TextUtils.htmlEncode((String) params.get("selectedProjectId"))
-                   + "\">create one</a>, and re-run this report. See also "
-                   + "<a href=\"http://jira.atlassian.com/browse/JRA-5042\">JRA-5042</a></span>";
-        }
-        String mapperName = (String) params.get("mapper");
-        final StatisticsMapper mapper = new FilterStatisticsValuesGenerator().getStatsMapper(mapperName);
-        final JiraServiceContext ctx = new JiraServiceContextImpl(authenticationContext.getUser());
-        final SearchRequest request = searchRequestService.getFilter(ctx, new Long(filterId));
-
-        final Map startingParams;
-        try
-        {
-            startingParams = EasyMap.build(
-                    "action", action,
-                    "statsGroup", getOptions(request, authenticationContext.getUser(), mapper),
-                    "searchRequest", request,
-                    "mapperType", mapperName,
-                    "customFieldManager", customFieldManager,
-                    "fieldVisibility", new FieldVisibilityBean(),
-                    "searchService", searchService,
-                    "portlet", this);
-            
-            startingParams.put("outlookDate", outlookDateManager.getOutlookDate(authenticationContext.getLocale()));
-
-            return descriptor.getHtml("view", startingParams);
-        }
-        catch (PermissionException e)
-        {
-            log.error(e, e);
-            return null;
-        }
-
-    }
-
-    public void validate(ProjectActionSupport action, Map params)
-    {
-        super.validate(action, params);
-        if (StringUtils.isEmpty((String) params.get("filterid")))
-        {
-            action.addError("filterid", action.getText("report.singlelevelgroupby.filter.is.required"));
-        }
-    }
-}

File src/main/java/com/atlassian/plugins/tutorial/jira/reports/CreationReport.java

+package com.atlassian.plugins.tutorial.jira.reports;
+
+import com.atlassian.jira.plugin.report.impl.AbstractReport;
+import com.atlassian.jira.web.action.ProjectActionSupport;
+
+
+import com.atlassian.core.util.DateUtils;
+import com.atlassian.jira.issue.search.SearchException;
+import com.atlassian.jira.issue.search.SearchProvider;
+import com.atlassian.jira.jql.builder.JqlQueryBuilder;
+import com.atlassian.jira.project.ProjectManager;
+import com.atlassian.jira.util.I18nHelper;
+import com.atlassian.jira.util.ParameterUtils;
+import com.atlassian.jira.web.bean.I18nBean;
+
+import com.atlassian.jira.web.util.OutlookDate;
+import com.atlassian.jira.web.util.OutlookDateManager;
+import com.atlassian.query.Query;
+
+import com.atlassian.crowd.embedded.api.User;
+
+import org.apache.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generate a histogram displaying number of issues opened in a specified period.
+ * The time period is divided by the specifed value for the histogram display.
+ */
+public class CreationReport extends AbstractReport
+{
+    private static final Logger log = Logger.getLogger(CreationReport.class);
+
+    // The max height for each bar in the histogram
+    private static final int MAX_HEIGHT = 200;
+    // Default interval value
+    private Long DEFAULT_INTERVAL = new Long(7);
+
+    // The highest issue count encountered in a search
+    private long maxCount = 0;
+    // A collection of issue open counts
+    private Collection<Long> openIssueCounts = new ArrayList<Long>();
+    // A collection of interval start dates - correlating with the openIssueCount collection.
+    private Collection<Date> dates = new ArrayList<Date>();
+
+    private final SearchProvider searchProvider;
+    private final OutlookDateManager outlookDateManager;
+    private final ProjectManager projectManager;
+
+    public CreationReport(SearchProvider searchProvider, OutlookDateManager outlookDateManager, ProjectManager projectManager)
+    {
+        this.searchProvider = searchProvider;
+        this.outlookDateManager = outlookDateManager;
+        this.projectManager = projectManager;
+    }
+
+    // Generate the report
+    public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception
+    {
+        User remoteUser = action.getRemoteUser();
+        I18nHelper i18nBean = new I18nBean(remoteUser);
+
+        // Retrieve the project parameter
+        Long projectId = ParameterUtils.getLongParam(params, "selectedProjectId");
+        // Retrieve the start and end dates and the time interval specified by the user
+        Date startDate = ParameterUtils.getDateParam(params, "startDate", i18nBean.getLocale());
+        Date endDate = ParameterUtils.getDateParam(params, "endDate", i18nBean.getLocale());
+        Long interval = ParameterUtils.getLongParam(params, "interval");
+
+        // Ensure that the interval is valid
+        if (interval == null || interval.longValue() <= 0)
+        {
+            interval = DEFAULT_INTERVAL;
+            log.error(action.getText("report.issuecreation.default.interval"));
+        }
+
+        getIssueCount(startDate, endDate, interval, remoteUser, projectId);
+
+        List<Number> normalCount = new ArrayList<Number>();
+
+        // Normalise the counts for the max height
+        if (maxCount != MAX_HEIGHT && maxCount > 0)
+        {
+            for (Long asLong : openIssueCounts)
+            {
+                Float floatValue = new Float((asLong.floatValue() / maxCount) * MAX_HEIGHT);
+                // Round it back to an integer
+                Integer newValue = new Integer(floatValue.intValue());
+
+                normalCount.add(newValue);
+            }
+        }
+
+        if (maxCount < 0)
+            action.addErrorMessage(action.getText("report.issuecreation.error"));
+
+        // Pass the issues to the velocity template
+        Map<String, Object> velocityParams = new HashMap<String, Object>();
+        velocityParams.put("startDate", startDate);
+        velocityParams.put("endDate", endDate);
+        velocityParams.put("openCount", openIssueCounts);
+        velocityParams.put("normalisedCount", normalCount);
+        velocityParams.put("dates", dates);
+        velocityParams.put("maxHeight", new Integer(MAX_HEIGHT));
+        velocityParams.put("outlookDate", outlookDateManager.getOutlookDate(i18nBean.getLocale()));
+        velocityParams.put("projectName", projectManager.getProjectObj(projectId).getName());
+        velocityParams.put("interval", interval);
+
+        return descriptor.getHtml("view", velocityParams);
+    }
+
+    // Retrieve the issues opened during the time period specified.
+    private long getOpenIssueCount(User remoteUser, Date startDate, Date endDate, Long projectId) throws SearchException
+    {
+        JqlQueryBuilder queryBuilder = JqlQueryBuilder.newBuilder();
+        Query query = queryBuilder.where().createdBetween(startDate, endDate).and().project(projectId).buildQuery();
+
+        return searchProvider.searchCount(query, remoteUser);
+    }
+
+    private void getIssueCount(Date startDate, Date endDate, Long interval, User remoteUser, Long projectId) throws SearchException
+    {
+        // Calculate the interval value in milliseconds
+        long intervalValue = interval.longValue() * DateUtils.DAY_MILLIS;
+        Date newStartDate;
+        long count = 0;
+
+        // Split the specified time period by the interval value
+        while (startDate.before(endDate))
+        {
+            newStartDate = new Date(startDate .getTime() + intervalValue);
+
+            // Retrieve the issues opened within the time interval
+            if (newStartDate.after(endDate))
+                count = getOpenIssueCount(remoteUser, startDate, endDate, projectId);
+            else
+                count = getOpenIssueCount(remoteUser, startDate, newStartDate, projectId);
+
+            // Store the highest count for normalisation of results
+            if (maxCount < count)
+                maxCount = count;
+
+            // Store the count and the start date for this period
+            openIssueCounts.add(new Long(count));
+            dates.add(startDate);
+
+            // Move start date to next period
+            startDate = newStartDate;
+        }
+    }
+
+    // Validate the parameters set by the user.
+    public void validate(ProjectActionSupport action, Map params)
+    {
+        User remoteUser = action.getRemoteUser();
+        I18nHelper i18nBean = new I18nBean(remoteUser);
+
+        Date startDate = ParameterUtils.getDateParam(params, "startDate", i18nBean.getLocale());
+        Date endDate = ParameterUtils.getDateParam(params, "endDate", i18nBean.getLocale());
+        Long interval = ParameterUtils.getLongParam(params, "interval");
+        Long projectId = ParameterUtils.getLongParam(params, "selectedProjectId");
+
+        OutlookDate outlookDate = outlookDateManager.getOutlookDate(i18nBean.getLocale());
+
+        if (startDate == null || !outlookDate.isDatePickerDate(outlookDate.formatDMY(startDate)))
+            action.addError("startDate", action.getText("report.issuecreation.startdate.required"));
+
+        if (endDate == null || !outlookDate.isDatePickerDate(outlookDate.formatDMY(endDate)))
+            action.addError("endDate", action.getText("report.issuecreation.enddate.required"));
+
+        if (interval == null || interval.longValue() <= 0)
+            action.addError("interval", action.getText("report.issuecreation.interval.invalid"));
+
+        if (projectId == null)
+            action.addError("selectedProjectId", action.getText("report.issuecreation.projectid.invalid"));
+
+        // The end date must be after the start date
+        if (startDate != null && endDate != null && endDate.before(startDate))
+        {
+            action.addError("endDate", action.getText("report.issuecreation.before.startdate"));
+        }
+    }
+}

File src/main/java/com/atlassian/plugins/tutorial/jira/reports/SingleLevelGroupByReportExtended.java

+package com.atlassian.plugins.tutorial.jira.reports;
+
+import com.atlassian.core.util.map.EasyMap;
+import com.atlassian.crowd.embedded.api.User;
+import com.atlassian.jira.bc.JiraServiceContext;
+import com.atlassian.jira.bc.JiraServiceContextImpl;
+import com.atlassian.jira.bc.filter.SearchRequestService;
+import com.atlassian.jira.bc.issue.search.SearchService;
+import com.atlassian.jira.exception.PermissionException;
+import com.atlassian.jira.issue.CustomFieldManager;
+import com.atlassian.jira.issue.IssueFactory;
+import com.atlassian.jira.issue.index.IssueIndexManager;
+import com.atlassian.jira.issue.search.ReaderCache;
+import com.atlassian.jira.issue.search.SearchException;
+import com.atlassian.jira.issue.search.SearchProvider;
+import com.atlassian.jira.issue.search.SearchRequest;
+import com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator;
+import com.atlassian.jira.issue.statistics.StatisticsMapper;
+import com.atlassian.jira.issue.statistics.StatsGroup;
+import com.atlassian.jira.issue.statistics.util.OneDimensionalDocIssueHitCollector;
+import com.atlassian.jira.plugin.report.impl.AbstractReport;
+import com.atlassian.jira.security.JiraAuthenticationContext;
+import com.atlassian.jira.util.SimpleErrorCollection;
+import com.atlassian.jira.web.FieldVisibilityManager;
+import com.atlassian.jira.web.action.ProjectActionSupport;
+import com.atlassian.jira.web.bean.FieldVisibilityBean;
+import com.atlassian.jira.web.bean.PagerFilter;
+import com.atlassian.jira.web.util.OutlookDateManager;
+import com.atlassian.util.profiling.UtilTimerStack;
+import com.opensymphony.util.TextUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.apache.lucene.search.Collector;
+import java.util.Map;
+
+public class SingleLevelGroupByReportExtended extends AbstractReport
+{
+    private static final Logger log = Logger.getLogger(SingleLevelGroupByReportExtended.class);
+
+    private final SearchProvider searchProvider;
+    private final JiraAuthenticationContext authenticationContext;
+    private final SearchRequestService searchRequestService;
+    private final IssueFactory issueFactory;
+    private final CustomFieldManager customFieldManager;
+    private final IssueIndexManager issueIndexManager;
+    private final SearchService searchService;
+    private final FieldVisibilityManager fieldVisibilityManager;
+    private final ReaderCache readerCache;
+    private final OutlookDateManager outlookDateManager;
+
+    public SingleLevelGroupByReportExtended(final SearchProvider searchProvider, final JiraAuthenticationContext authenticationContext,
+                                            final SearchRequestService searchRequestService, final IssueFactory issueFactory,
+                                            final CustomFieldManager customFieldManager, final IssueIndexManager issueIndexManager,
+                                            final SearchService searchService, final FieldVisibilityManager fieldVisibilityManager,
+                                            final ReaderCache readerCache, final OutlookDateManager outlookDateManager )
+    {
+        this.searchProvider = searchProvider;
+        this.authenticationContext = authenticationContext;
+        this.searchRequestService = searchRequestService;
+        this.issueFactory = issueFactory;
+        this.customFieldManager = customFieldManager;
+        this.issueIndexManager = issueIndexManager;
+        this.searchService = searchService;
+        this.fieldVisibilityManager = fieldVisibilityManager;
+        this.readerCache = readerCache;
+        this.outlookDateManager =  outlookDateManager;
+    }
+
+    public StatsGroup getOptions(SearchRequest sr, User user, StatisticsMapper mapper) throws PermissionException
+    {
+
+        try
+        {
+            return searchMapIssueKeys(sr, user, mapper);
+        }
+        catch (SearchException e)
+        {
+            log.error("Exception rendering " + this.getClass().getName() + ".  Exception " + e.getMessage(), e);
+            return null;
+        }
+    }
+
+    public StatsGroup searchMapIssueKeys(SearchRequest request, User searcher, StatisticsMapper mapper)
+            throws SearchException
+    {
+        try
+        {
+            UtilTimerStack.push("Search Count Map");
+            StatsGroup statsGroup = new StatsGroup(mapper);
+            Collector hitCollector = new OneDimensionalDocIssueHitCollector(mapper.getDocumentConstant(), statsGroup,
+                    issueIndexManager.getIssueSearcher().getIndexReader(), issueFactory,
+                    fieldVisibilityManager, readerCache);
+            searchProvider.searchAndSort((request != null) ? request.getQuery() : null, searcher, hitCollector, PagerFilter.getUnlimitedFilter());
+            return statsGroup;
+        }
+        finally
+        {
+            UtilTimerStack.pop("Search Count Map");
+        }
+    }
+
+    public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception
+    {
+        String filterId = (String) params.get("filterid");
+        if (filterId == null)
+        {
+            log.error("Single Level Group By Report run without a project selected (JRA-5042): params=" + params);
+            return "<span class='errMsg'>No search filter has been selected. Please "
+                    + "<a href=\"IssueNavigator.jspa?reset=Update&amp;pid="
+                    + TextUtils.htmlEncode((String) params.get("selectedProjectId"))
+                    + "\">create one</a>, and re-run this report. See also "
+                    + "<a href=\"http://jira.atlassian.com/browse/JRA-5042\">JRA-5042</a></span>";
+        }
+        String mapperName = (String) params.get("mapper");
+        final StatisticsMapper mapper = new FilterStatisticsValuesGenerator().getStatsMapper(mapperName);
+        final JiraServiceContext ctx = new JiraServiceContextImpl(authenticationContext.getLoggedInUser());
+        final SearchRequest request = searchRequestService.getFilter(ctx, new Long(filterId));
+
+        final Map startingParams;
+        try
+        {
+            startingParams = EasyMap.build(
+                    "action", action,
+                    "statsGroup", getOptions(request, authenticationContext.getLoggedInUser(), mapper),
+                    "searchRequest", request,
+                    "mapperType", mapperName,
+                    "customFieldManager", customFieldManager,
+                    "fieldVisibility", new FieldVisibilityBean(),
+                    "searchService", searchService,
+                    "portlet", this);
+            startingParams.put("outlookDate", 
+                  outlookDateManager.getOutlookDate(authenticationContext.getLocale()));
+            return descriptor.getHtml("view", startingParams);
+
+        }
+        catch (PermissionException e)
+        {
+            log.error(e, e);
+            return null;
+        }
+    }
+
+    public void validate(ProjectActionSupport action, Map params)
+    {
+        super.validate(action, params);
+        String filterId = (String) params.get("filterid");
+        if (StringUtils.isEmpty(filterId))
+        {
+            action.addError("filterid", action.getText("report.singlelevelgroupby.filter.is.required"));
+        }
+        else
+        {
+            validateFilterId(action,filterId);
+        }
+    }
+
+    private void validateFilterId(ProjectActionSupport action, String filterId)
+    {
+        try
+        {
+            JiraServiceContextImpl serviceContext = new JiraServiceContextImpl(
+                    action.getLoggedInUser(), new SimpleErrorCollection());
+            SearchRequest searchRequest = searchRequestService.getFilter(serviceContext, new Long(filterId));
+            if (searchRequest == null)
+            {
+                action.addErrorMessage(action.getText("report.error.no.filter"));
+            }
+        }
+        catch (NumberFormatException nfe)
+        {
+            action.addError("filterId", action.getText("report.error.filter.id.not.a.number", filterId));
+        }
+    }
+}

File src/main/resources/CreationReport.properties

+report.issuecreation.label = Issue Creation Report
+report.issuecreation.name = Issue Creation Report
+report.issuecreation.projectid.name = Project
+report.issuecreation.projectid.description = Select the project to display report on.
+report.issuecreation.description = Report displaying a histogram of issues opened over a specified period.
+report.issuecreation.startdate = Start Date
+report.issuecreation.startdate.description = Graph all issues created after this date.
+report.issuecreation.enddate = End Date
+report.issuecreation.enddate.description = Graph all issues created before this date.
+report.issuecreation.interval = Interval
+report.issuecreation.interval.days = days
+report.issuecreation.interval.description = Specify the interval (in days) for the report.
+report.issuecreation.startdate.required = A valid "Start Date" is required to generate this report.
+report.issuecreation.enddate.required = A valid "End Date" is required to generate this report.
+report.issuecreation.interval.invalid = The interval must be a number greater than 0.
+report.issuecreation.before.startdate = The "End Date" must be after the "Start Date".
+report.issuecreation.error = Error occurred generating Issue Creation Report.
+report.issuecreation.projectid.invalid = Please select a valid project.
+report.issuecreation.default.interval = The interval specified is invalid - using default interval.
+report.issuecreation.duration = Duration
+report.issuecreation.project = Project

File src/main/resources/SingleLevelGroupByReportExtended.properties

+report.singlelevelgroupby.label.extended = Single Level Group By Report Extended
+report.singlelevelgroupby.filterId = Filter
+report.singlelevelgroupby.filterId.description = Select a filter to display
+report.singlelevelgroupby.mapper = Statistic Type
+report.singlelevelgroupby.mapper.description = Select a field to group by
+report.singlelevelgroupby.mapper.filterid.name = Filter
+report.singlelevelgroupby.description = This report allows you to display issues grouped by a certain field

File src/main/resources/atlassian-plugin.xml

-<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.artifactId}" plugins-version="2">
-    <plugin-info>
-        <description>${project.description}</description>
-        <version>${project.version}</version>
-        <vendor name="${project.organization.name}" url="${project.organization.url}" />
-    </plugin-info>
+<?xml version="1.0" encoding="UTF-8"?>
 
-    <!-- An simple example of customising an exisiting report - adding the admin and updated date of an issue to the template -->
-    <report key="singlelevelgroupbyextended" name="Example: Group By Report Extended" class="com.atlassian.plugins.tutorial.jira.report.SingleLevelGroupByReportExtended">
-        <description key="report.singlelevelgroupby.description">i18n description</description>
-        <resource type="velocity" name="view" location="templates/groupreport/single-groupby-report-extended.vm" />
-        <resource type="i18n" name="i18n" location="com.atlassian.plugins.tutorial.jira.report.singlelevelgroup_report" />
-        <label key="report.singlelevelgroupby.label.extended" />
-        <properties>
-            <property>
-                <key>filterid</key>
-                <name>report.singlelevelgroupby.filterId</name>
-                <description>report.singlelevelgroupby.filterId.description</description>
-                <type>select</type>
-                <values class="com.atlassian.jira.portal.SearchRequestValuesGenerator" />
-            </property>
-            <property>
-                <key>mapper</key>
-                <name>report.singlelevelgroupby.mapper</name>
-                <description>report.singlelevelgroupby.mapper.description</description>
-                <type>select</type>
-                <values class="com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator" />
-            </property>
-        </properties>
-    </report>
-
-    <!-- An 'Issue Creation' Report - displays a histogram of issue create within a specifed project over a specified time -->
-    <report key="issuecreationreport" name="Example: Issue Creation Report" class="com.atlassian.plugins.tutorial.jira.report.CreationReport">
-        <description key="report.issuecreation.description">i18n description</description>
-        <label key="report.issuecreation.label" />
-
-        <resource type="velocity" name="view" location="templates/creationreport/issuecreation-report.vm" />
-        <resource type="i18n" name="i18n" location="com.atlassian.plugins.tutorial.jira.report.issuecreation_report" />
-
-        <properties>
-            <property>
-                <key>projectid</key>
-                <name>report.issuecreation.projectid.name</name>
-                <description>report.issuecreation.projectid.description</description>
-                <type>select</type>
-                <values class="com.atlassian.jira.portal.ProjectValuesGenerator"/>
-            </property>
-            <property>
-                <key>startDate</key>
-                <name>report.issuecreation.startdate</name>
-                <description>report.issuecreation.startdate.description</description>
-                <type>date</type>
-            </property>
-            <property>
-                <key>endDate</key>
-                <name>report.issuecreation.enddate</name>
-                <description>report.issuecreation.enddate.description</description>
-                <type>date</type>
-            </property>
-            <property>
-                <key>interval</key>
-                <name>report.issuecreation.interval</name>
-                <description>report.issuecreation.interval.description</description>
-                <type>long</type>
-                <default>3</default>
-            </property>
-        </properties>
-    </report>
-    
-</atlassian-plugin>
+<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
+  <plugin-info>
+    <description>${project.description}</description>
+    <version>${project.version}</version>
+    <vendor name="${project.organization.name}" url="${project.organization.url}"/>
+    <param name="plugin-icon">images/pluginIcon.png</param>
+    <param name="plugin-logo">images/pluginLogo.png</param>
+  </plugin-info>
+  <!-- add our i18n resource -->
+  <resource type="i18n" name="i18n" location="jira-report-plugin"/>
+  <!-- add our web resources -->
+  <web-resource key="jira-report-plugin-resources" name="jira-report-plugin Web Resources">
+    <dependency>com.atlassian.auiplugin:ajs</dependency>
+    <resource type="download" name="jira-report-plugin.css" location="/css/jira-report-plugin.css"/>
+    <resource type="download" name="jira-report-plugin.js" location="/js/jira-report-plugin.js"/>
+    <resource type="download" name="images/" location="/images"/>
+    <context>jira-report-plugin</context>
+  </web-resource>
+  <!-- publish our component -->
+  <component key="myPluginComponent" class="com.atlassian.plugins.tutorial.jira.MyPluginComponentImpl" public="true">
+    <interface>com.atlassian.plugins.tutorial.jira.MyPluginComponent</interface>
+  </component>
+  <!-- import from the product container -->
+  <component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
+  <report name="Single Level Group By Report Extended" i18n-name-key="single-level-group-by-report-extended.name" key="single-level-group-by-report-extended" class="com.atlassian.plugins.tutorial.jira.reports.SingleLevelGroupByReportExtended">
+    <description key="single-level-group-by-report-extended.description">The Single Level Group By Report Extended Plugin</description>
+    <resource name="view" type="velocity" location="/templates/reports/single-level-group-by-report-extended/view.vm"/>
+    <resource name="i18n" type="i18n" location="SingleLevelGroupByReportExtended"/>
+    <label key="single-level-group-by-report-extended.label"/>
+   <properties>
+         <property>
+             <key>filterid</key>
+             <name>report.singlelevelgroupby.filterId</name>
+             <description>report.singlelevelgroupby.filterId.description</description>
+             <type>filterpicker</type>
+             <i18n>false</i18n>
+         </property>
+         <property>
+             <key>mapper</key>
+             <name>report.singlelevelgroupby.mapper</name>
+             <description>report.singlelevelgroupby.mapper.description</description>
+             <type>select</type>
+             <values class="com.atlassian.jira.issue.statistics.FilterStatisticsValuesGenerator" />
+         </property>
+    </properties>
+  </report>
+  <report name="Creation Report" i18n-name-key="creation-report.name" key="creation-report" class="com.atlassian.plugins.tutorial.jira.reports.CreationReport">
+    <description key="creation-report.description">The Creation Report Plugin</description>
+    <resource name="view" type="velocity" location="/templates/reports/creation-report/view.vm"/>
+    <resource name="i18n" type="i18n" location="CreationReport"/>
+    <label key="creation-report.label"></label>
+    <properties>
+         <property>
+             <key>projectid</key>
+             <name>report.issuecreation.projectid.name</name>
+             <description>report.issuecreation.projectid.description</description>
+             <type>filterprojectpicker</type>
+         </property>
+         <property>
+             <key>startDate</key>
+             <name>report.issuecreation.startdate</name>
+             <description>report.issuecreation.startdate.description</description>
+             <type>date</type>
+         </property>
+         <property>
+             <key>endDate</key>
+             <name>report.issuecreation.enddate</name>
+             <description>report.issuecreation.enddate.description</description>
+             <type>date</type>
+         </property>
+         <property>
+             <key>interval</key>
+             <name>report.issuecreation.interval</name>
+             <description>report.issuecreation.interval.description</description>
+             <type>long</type>
+             <default>3</default>
+         </property>
+   </properties>
+  </report>
+</atlassian-plugin>

File src/main/resources/com/atlassian/plugins/tutorial/jira/report/issuecreation_report.properties

-report.issuecreation.label = Issue Creation Report
-report.issuecreation.name = Issue Creation Report
-report.issuecreation.projectid.name = Project
-report.issuecreation.projectid.description = Select the project to display report on.
-report.issuecreation.description = Report displaying a histogram of issues opened over a specified period.
-report.issuecreation.startdate = Start Date
-report.issuecreation.startdate.description = Graph all issues created after this date.
-report.issuecreation.enddate = End Date
-report.issuecreation.enddate.description = Graph all issues created before this date.
-report.issuecreation.interval = Interval
-report.issuecreation.interval.days = days
-report.issuecreation.interval.description = Specify the interval (in days) for the report.
-report.issuecreation.startdate.required = A valid "Start Date" is required to generate this report.
-report.issuecreation.enddate.required = A valid "End Date" is required to generate this report.
-report.issuecreation.interval.invalid = The interval must be a number greater than 0.
-report.issuecreation.before.startdate = The "End Date" must be after the "Start Date".
-report.issuecreation.error = Error occurred generating Issue Creation Report.
-report.issuecreation.projectid.invalid = Please select a valid project.
-report.issuecreation.default.interval = The interval specified is invalid - using default interval.
-report.issuecreation.duration = Duration
-report.issuecreation.project = Project

File src/main/resources/com/atlassian/plugins/tutorial/jira/report/singlelevelgroup_report.properties

-report.singlelevelgroupby.label.extended = Single Level Group By Report Extended
-report.singlelevelgroupby.filterId = Filter
-report.singlelevelgroupby.filterId.description = Select a filter to display
-report.singlelevelgroupby.mapper = Statistic Type
-report.singlelevelgroupby.mapper.description = Select a field to group by
-report.singlelevelgroupby.mapper.filterid.name = Filter
-
-report.singlelevelgroupby.description = This report allows you to display issues grouped by a certain field

File src/main/resources/css/jira-report-plugin.css

Empty file added.

File src/main/resources/images/pluginIcon.png

Added
New image

File src/main/resources/images/pluginLogo.png

Added
New image

File src/main/resources/jira-report-plugin.properties

+#put any key/value pairs here
+my.plugin.name=MyPlugin
+single-level-group-by-report-extended.label=Single Level Group By Report Extended
+single-level-group-by-report-extended.name=Single Level Group By Report Extended
+single-level-group-by-report-extended.description=The Single Level Group By Report Extended Plugin
+
+creation-report.label=Creation Report
+creation-report.name=Creation Report
+creation-report.description=The Creation Report Plugin

File src/main/resources/js/jira-report-plugin.js

Empty file added.

File src/main/resources/templates/creationreport/issuecreation-report.vm

-<div style="padding: 5px">
-<!-- Display the report configuration -->
-<h4>
-    $i18n.getText('report.issuecreation.project'): $projectName | 
-    $i18n.getText('report.issuecreation.duration'): $outlookDate.formatDMY($startDate) - $outlookDate.formatDMY($endDate) |
-    $i18n.getText('report.issuecreation.interval'): $interval $i18n.getText('report.issuecreation.interval.days')
-</h4>
-<br />
-<table style="width: 100%; border: 0; background-color: lightgrey">
-    <!-- Create a row to display the bars-->
-    <tr valign="bottom" style="background-color: white; padding: 1px">
-        #foreach ($normalCount in $normalisedCount)
-            <td height="$maxHeight" align="center">
-            #if ($normalCount == 0)
-                &nbsp;
-            #else
-                <img src="${baseurl}/images/bluepixel.gif" width="12" height="$normalCount">
-            #end
-            </td>
-        #end
-    </tr>
-    <!-- Have one row for the issue count -->
-    <tr style="background-color: #eee; padding: 1px">
-        #foreach ($count in $openCount)
-            <td align="center"><b>$count</b></td>
-        #end
-    </tr>
-    <!-- And one row to display the date -->
-    <tr style="background-color: #eee; padding: 1px">
-        #foreach ($date in $dates)
-            <td align="center"><b>$outlookDate.formatDMY($date)</b></td>
-        #end
-    </tr>
-</table>
-</div>

File src/main/resources/templates/groupreport/single-groupby-report-extended.vm

-#if ($searchRequest)
-    #set ($urlPrefix = "${req.contextPath}/secure/IssueNavigator.jspa?reset=true")
-#end
-<table width="100%" class="report" id="single_groupby_report_table">
-<tr><th class="reportHeading" colspan="9">
-    <h3 class="formtitle">$i18n.getText('report.singlelevelgroupby.mapper.filterid.name'): $textutils.htmlEncode($searchRequest.name)</h3>
-</th></tr>
-    #foreach ($option in $statsGroup.entrySet())
-    #set ($issues = $option.value)
-
-        <tr>
-            <th colspan="9" class="subHeading">
-                <h3 class="bluetext">
-                #statHeading ($mapperType $option.key $customFieldManager "${urlPrefix}$!searchService.getQueryString($user, $statsGroup.getMapper().getSearchUrlSuffix($option.key, $searchRequest).getQuery())")
-                </h3>
-
-                #set ($graphModel = $statsGroup.getResolvedIssues($option.key))
-
-                #percentageGraphDiv ($graphModel)
-                <span class="graphLabel">$i18n.getText("common.words.progress"):</span>
-                <br />
-                #if ($issues.size() > 0)
-                    <span class="graphDescription">$i18n.getText("roadmap.issuesresolved", "$statsGroup.getResolvedIssueCount($issues)", "$issues.size()")</span>
-                #end
-            </th>
-        </tr>
-
-        #if ($issues.size() > 0)
-            #foreach ($issue in $issues)
-                <tr>
-                    <td width="5%">&nbsp;</td>
-                    #issueLineItem ($issue)
-                    <td nowrap>
-                        #if($issue.getAssignee())
-                            $issue.getAssignee().getFullName()
-                        #else
-                            $i18n.getText('common.concepts.unassigned')
-                        #end</td>
-                    <td nowrap>$outlookDate.format($issue.getUpdated())</td>
-                </tr>
-            #end
-        #else
-            <tr>
-                <td>&nbsp;</td>
-                <td colspan="8">
-                    <span class="subText">$action.getText("common.concepts.noissues").</span>
-                </td>
-            </tr>
-        #end
-    #end
-
-    ## Render the Irrelevant issues if there are any
-    #if($statsGroup.getIrrelevantIssues().size() > 0)
-        #set ($issues = $statsGroup.getIrrelevantIssues())
-        <tr>
-            <th colspan="9" class="subHeading">
-                <h3 class="bluetext">
-                    <span title="$i18n.getText('common.concepts.irrelevant.desc')">$i18n.getText('common.concepts.irrelevant')</span>
-                </h3>
-
-                #set ($graphModel = $statsGroup.getIrrelevantResolvedIssues())
-
-                #percentageGraphDiv ($graphModel)
-                <span class="graphLabel">$i18n.getText("common.words.progress"):</span>
-                <br />
-                #if ($issues.size() > 0)
-                    <span class="graphDescription">$i18n.getText("roadmap.issuesresolved", "$statsGroup.getResolvedIssueCount($issues)", "$issues.size()")</span>
-                #end
-            </th>
-        </tr>
-
-        #if ($issues.size() > 0)
-            #foreach ($issue in $issues)
-                <tr>
-                    <td width="5%">&nbsp;</td>
-                    #issueLineItem ($issue)
-                    <td nowrap>
-                        #if($issue.getAssignee())
-                            $issue.getAssignee().getFullName()
-                        #else
-                            $i18n.getText('common.concepts.unassigned')
-                        #end</td>
-                    <td nowrap>$outlookDate.format($issue.getUpdated())</td>
-                </tr>
-            #end
-        #else
-            <tr>
-                <td>&nbsp;</td>
-                <td colspan="8">
-                    <span class="subText">$action.getText("common.concepts.noissues").</span>
-                </td>
-            </tr>
-        #end
-
-    #end
-
-</table>

File src/main/resources/templates/reports/creation-report/view.vm

+<div style="padding: 5px">
+<!-- Display the report configuration -->
+<h4>
+    $i18n.getText('report.issuecreation.project'): $projectName | 
+    $i18n.getText('report.issuecreation.duration'): $outlookDate.formatDMY($startDate) - $outlookDate.formatDMY($endDate) |
+    $i18n.getText('report.issuecreation.interval'): $interval $i18n.getText('report.issuecreation.interval.days')
+</h4>
+<br />
+<table style="width: 100%; border: 0; background-color: lightgrey">
+    <!-- Create a row to display the bars-->
+    <tr valign="bottom" style="background-color: white; padding: 1px">
+        #foreach ($normalCount in $normalisedCount)
+            <td height="$maxHeight" align="center">
+            #if ($normalCount == 0)
+                &nbsp;
+            #else
+                <img src="${baseurl}/images/bluepixel.gif" width="12" height="$normalCount">
+            #end
+            </td>
+        #end
+    </tr>
+    <!-- Have one row for the issue count -->
+    <tr style="background-color: #eee; padding: 1px">
+        #foreach ($count in $openCount)
+            <td align="center"><b>$count</b></td>
+        #end
+    </tr>
+    <!-- And one row to display the date -->
+    <tr style="background-color: #eee; padding: 1px">
+        #foreach ($date in $dates)
+            <td align="center"><b>$outlookDate.formatDMY($date)</b></td>
+        #end
+    </tr>
+</table>
+</div>

File src/main/resources/templates/reports/single-level-group-by-report-extended/view.vm

+#enable_html_escaping()
+#if ($searchRequest)
+    #set ($urlPrefix = "${req.contextPath}/secure/IssueNavigator.jspa?reset=true")
+#end
+<table width="100%" class="aui" id="single_groupby_report_table">
+    <thead>
+        <tr>
+            <th colspan="7">
+                <h2>$i18n.getText('report.singlelevelgroupby.mapper.filterid.name'): $textutils.htmlEncode($searchRequest.name)</h2>
+            </th>
+        </tr>
+    </thead>
+    <tbody>
+        #foreach ($option in $statsGroup.entrySet())
+            #set ($issues = $option.value)
+            #set ($graphModel = $statsGroup.getResolvedIssues($option.key))
+            <tr>
+                <th colspan="8" class="stat-heading">
+                    <div class="stat-progress">
+                        <span class="graphLabel">$i18n.getText("common.words.progress"):</span>
+                        #percentageGraphDiv ($graphModel)
+                        #if ($issues.size() > 0)
+                            <span class="graphDescription">$i18n.getText("roadmap.issuesresolved", "$statsGroup.getResolvedIssueCount($issues)", "$issues.size()")</span>
+                        #end
+                    </div>
+                    <h3>#statHeading ($mapperType $option.key $customFieldManager "${urlPrefix}$!searchService.getQueryString($user, $statsGroup.getMapper().getSearchUrlSuffix($option.key, $searchRequest).getQuery())")</h3>
+                </th>
+            </tr>
+            #if ($issues.size() > 0)
+                #foreach ($issue in $issues)
+                <tr>
+                    <td width="5%">&nbsp;</td>
+                    #issueLineItem ($issue)
+                    <td nowrap>
+                        #if($issue.getAssignee())
+                            $issue.getAssignee().getDisplayName()
+                        #else
+                            $i18n.getText('common.concepts.unassigned')
+                        #end</td>
+                    <td nowrap>$outlookDate.format($issue.getUpdated())</td>
+                </tr>
+                #end
+            #else
+                <tr>
+                    <td colspan="6">
+                        <span class="subText">$action.getText("common.concepts.noissues").</span>
+                    </td>
+                </tr>
+            #end
+        #end
+        ## Render the Irrelevant issues if there are any
+        #if($statsGroup.getIrrelevantIssues().size() > 0)
+            #set ($issues = $statsGroup.getIrrelevantIssues())
+            #set ($graphModel = $statsGroup.getIrrelevantResolvedIssues())
+            <tr>
+                <th colspan="7">
+                    <div class="stat-progress">
+                        <span class="graphLabel">$i18n.getText("common.words.progress"):</span>
+                        #percentageGraphDiv ($graphModel)
+                        #if ($issues.size() > 0)
+                            <span class="graphDescription">$i18n.getText("roadmap.issuesresolved", "$statsGroup.getResolvedIssueCount($issues)", "$issues.size()")</span>
+                        #end
+                    </div>
+                    <h3><span title="$i18n.getText('common.concepts.irrelevant.desc')">$i18n.getText('common.concepts.irrelevant')</span></h3>
+                </th>
+            </tr>
+            #if ($issues.size() > 0)
+                #foreach ($issue in $issues)
+                <tr>
+                     <td width="5%">&nbsp;</td>
+                     #issueLineItem ($issue)
+                     <td nowrap>
+                         #if($issue.getAssignee())
+                             $issue.getAssignee().getDisplayName()
+                         #else
+                             $i18n.getText('common.concepts.unassigned')
+                         #end</td>
+                     <td nowrap>$outlookDate.format($issue.getUpdated())</td>
+                 </tr>
+                 #end
+            #else
+                <tr>
+                    <td colspan="7">
+                        <span class="subText">$action.getText("common.concepts.noissues").</span>
+                    </td>
+                </tr>
+            #end
+        #end
+    </tbody>
+</table>

File src/test/java/com/atlassian/tutorial/jira/report/MyPluginTest.java

-package com.atlassian.tutorial.jira.report;
-
-import org.junit.Test;
-
-public class MyPluginTest
-{
-    @Test
-    public void testSomething()
-    {
-    }
-}

File src/test/java/it/MyPluginTest.java

-package it;
-
-import org.junit.Test;
-
-public class MyPluginTest
-{
-    @Test
-    public void integrationTest()
-    {
-    }
-}

File src/test/java/it/com/atlassian/plugins/tutorial/jira/MyComponentWiredTest.java

+package it.com.atlassian.plugins.tutorial.jira;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import com.atlassian.plugins.osgi.test.AtlassianPluginsTestRunner;
+import com.atlassian.plugins.tutorial.jira.MyPluginComponent;
+import com.atlassian.sal.api.ApplicationProperties;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AtlassianPluginsTestRunner.class)
+public class MyComponentWiredTest
+{
+    private final ApplicationProperties applicationProperties;
+    private final MyPluginComponent myPluginComponent;
+
+    public MyComponentWiredTest(ApplicationProperties applicationProperties,MyPluginComponent myPluginComponent)
+    {
+        this.applicationProperties = applicationProperties;
+        this.myPluginComponent = myPluginComponent;
+    }
+
+    @Test
+    public void testMyName()
+    {
+        assertEquals("names do not match!", "myComponent:" + applicationProperties.getDisplayName(),myPluginComponent.getName());
+    }
+}

File src/test/java/ut/com/atlassian/plugins/tutorial/jira/MyComponentUnitTest.java

+package ut.com.atlassian.plugins.tutorial.jira;
+
+import org.junit.Test;
+import com.atlassian.plugins.tutorial.jira.MyPluginComponent;
+import com.atlassian.plugins.tutorial.jira.MyPluginComponentImpl;
+
+import static org.junit.Assert.assertEquals;
+
+public class MyComponentUnitTest
+{
+    @Test
+    public void testMyName()
+    {
+        MyPluginComponent component = new MyPluginComponentImpl(null);
+        assertEquals("names do not match!", "myComponent",component.getName());
+    }
+}

File src/test/java/ut/com/atlassian/plugins/tutorial/jira/reports/CreationReportTest.java

+package ut.com.atlassian.plugins.tutorial.jira.reports;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import com.atlassian.plugins.tutorial.jira.reports.CreationReport;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @since 3.5
+ */
+public class CreationReportTest {
+
+    @Before
+    public void setup() {
+
+    }
+
+    @After
+    public void tearDown() {
+
+    }
+
+    @Test(expected=Exception.class)
+    public void testSomething() throws Exception {
+
+        //CreationReport testClass = new CreationReport();
+
+        throw new Exception("CreationReport has no tests!");
+
+    }
+
+}

File src/test/java/ut/com/atlassian/plugins/tutorial/jira/reports/SingleLevelGroupByReportExtendedTest.java

+package ut.com.atlassian.plugins.tutorial.jira.reports;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import com.atlassian.plugins.tutorial.jira.reports.SingleLevelGroupByReportExtended;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * @since 3.5
+ */
+public class SingleLevelGroupByReportExtendedTest {
+
+    @Before
+    public void setup() {
+
+    }
+
+    @After
+    public void tearDown() {
+
+    }
+
+    @Test(expected=Exception.class)
+    public void testSomething() throws Exception {
+
+        //SingleLevelGroupByReportExtended testClass = new SingleLevelGroupByReportExtended();
+
+        throw new Exception("SingleLevelGroupByReportExtended has no tests!");
+
+    }
+
+}

File src/test/resources/TEST_RESOURCES_README

-Create any of the test resources you might need in this directory.
-
-Please remove this file before releasing your plugin.

File src/test/resources/atlassian-plugin.xml

+<atlassian-plugin key="${project.groupId}.${project.artifactId}-tests" name="${project.name}" plugins-version="2">
+    <plugin-info>
+        <description>${project.description}</description>
+        <version>${project.version}</version>
+        <vendor name="${project.organization.name}" url="${project.organization.url}" />
+    </plugin-info>
+
+    <!-- from our base plugin -->
+    <component-import key="myComponent" interface="com.atlassian.plugins.tutorial.jira.MyPluginComponent"/>
+
+    <!-- from the product container -->
+    <component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties" />
+    
+</atlassian-plugin>

File src/test/xml/TEST_XML_RESOURCES_README

-Create all XML test resources here - these might be needed for populating JIRA instance at the integration-test phase with test data.
-
-Please remove this file before releasing your plugin.