package to.rtc.rtc2jira.exporter.jira;
import static to.rtc.rtc2jira.storage.Field.of;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.core.MediaType;
import to.rtc.rtc2jira.ExportManager;
import to.rtc.rtc2jira.Settings;
import to.rtc.rtc2jira.exporter.Exporter;
import to.rtc.rtc2jira.exporter.jira.entities.BulkCreateContainer;
import to.rtc.rtc2jira.exporter.jira.entities.BulkCreateEntry;
import to.rtc.rtc2jira.exporter.jira.entities.BulkCreateResponseEntity;
import to.rtc.rtc2jira.exporter.jira.entities.Issue;
import to.rtc.rtc2jira.exporter.jira.entities.IssueAttachment;
import to.rtc.rtc2jira.exporter.jira.entities.IssueComment;
import to.rtc.rtc2jira.exporter.jira.entities.IssueFields;
import to.rtc.rtc2jira.exporter.jira.entities.IssueResolution;
import to.rtc.rtc2jira.exporter.jira.entities.IssueSearch;
import to.rtc.rtc2jira.exporter.jira.entities.IssueSearch.IssueSearchResult;
import to.rtc.rtc2jira.exporter.jira.entities.IssueStatus;
import to.rtc.rtc2jira.exporter.jira.entities.IssueType;
import to.rtc.rtc2jira.exporter.jira.entities.JiraUser;
import to.rtc.rtc2jira.exporter.jira.entities.Project;
import to.rtc.rtc2jira.exporter.jira.entities.ResolutionEnum;
import to.rtc.rtc2jira.exporter.jira.entities.StateEnum;
import to.rtc.rtc2jira.exporter.jira.mapping.MappingRegistry;
import to.rtc.rtc2jira.exporter.jira.mapping.WorkItemTypeMapping;
import to.rtc.rtc2jira.storage.Attachment;
import to.rtc.rtc2jira.storage.AttachmentStorage;
import to.rtc.rtc2jira.storage.Comment;
import to.rtc.rtc2jira.storage.FieldNames;
import to.rtc.rtc2jira.storage.StorageEngine;
import to.rtc.rtc2jira.storage.StorageQuery;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.multipart.file.FileDataBodyPart;
public class JiraExporter implements Exporter {
private static final String DUMMY_SUMMARY_TEXT = "Dummy";
private static final Logger LOGGER = Logger.getLogger(JiraExporter.class.getName());
public static final JiraExporter INSTANCE;
private StorageEngine store;
private Settings settings;
private JiraRestAccess restAccess;
private Optional<Project> projectOptional;
private int highestExistingId = -1;
private MappingRegistry mappingRegistry;
private WorkItemTypeMapping workItemTypeMapping;
private Set<Integer> updatedItems = new HashSet<Integer>();
static private Set<Integer> _tempMovedItems = new HashSet<Integer>();
static {
INSTANCE = new JiraExporter();
LOGGER.addHandler(ExportManager.DEFAULT_LOG_HANDLER);
// florian
_tempMovedItems.add(Integer.valueOf(35392));
_tempMovedItems.add(Integer.valueOf(36385));
_tempMovedItems.add(Integer.valueOf(36068));
// franz
_tempMovedItems.add(Integer.valueOf(30379));
_tempMovedItems.add(Integer.valueOf(31428));
_tempMovedItems.add(Integer.valueOf(31858));
_tempMovedItems.add(Integer.valueOf(34034));
_tempMovedItems.add(Integer.valueOf(34035));
_tempMovedItems.add(Integer.valueOf(34477));
_tempMovedItems.add(Integer.valueOf(34549));
_tempMovedItems.add(Integer.valueOf(34663));
_tempMovedItems.add(Integer.valueOf(34887));
_tempMovedItems.add(Integer.valueOf(34888));
_tempMovedItems.add(Integer.valueOf(34889));
_tempMovedItems.add(Integer.valueOf(34890));
_tempMovedItems.add(Integer.valueOf(34892));
_tempMovedItems.add(Integer.valueOf(34893));
_tempMovedItems.add(Integer.valueOf(34895));
_tempMovedItems.add(Integer.valueOf(32606));
_tempMovedItems.add(Integer.valueOf(35964));
_tempMovedItems.add(Integer.valueOf(36132));
_tempMovedItems.add(Integer.valueOf(36133));
_tempMovedItems.add(Integer.valueOf(36134));
_tempMovedItems.add(Integer.valueOf(36135));
_tempMovedItems.add(Integer.valueOf(36189));
_tempMovedItems.add(Integer.valueOf(36192));
_tempMovedItems.add(Integer.valueOf(36193));
_tempMovedItems.add(Integer.valueOf(36194));
_tempMovedItems.add(Integer.valueOf(36241));
_tempMovedItems.add(Integer.valueOf(36242));
_tempMovedItems.add(Integer.valueOf(36284));
_tempMovedItems.add(Integer.valueOf(36480));
_tempMovedItems.add(Integer.valueOf(36544));
_tempMovedItems.add(Integer.valueOf(36696));
_tempMovedItems.add(Integer.valueOf(36761));
_tempMovedItems.add(Integer.valueOf(36774));
_tempMovedItems.add(Integer.valueOf(36793));
// unknown
_tempMovedItems.add(Integer.valueOf(33815));
_tempMovedItems.add(Integer.valueOf(36800));
_tempMovedItems.add(Integer.valueOf(36801));
// 19042016
_tempMovedItems.add(Integer.valueOf(36751));
_tempMovedItems.add(Integer.valueOf(36811));
}
private JiraExporter() {};
@Override
public boolean isConfigured() {
return Settings.getInstance().hasJiraProperties();
}
@Override
public void initialize(Settings settings, StorageEngine store) throws Exception {
this.settings = settings;
this.store = store;
setRestAccess(new JiraRestAccess(settings.getJiraUrl(), settings.getJiraUser(), settings.getJiraPassword()));
ClientResponse response = getRestAccess().get("/myself");
// ClientResponse response = getRestAccess().get("/issue/WOR-137");
if (response.getStatus() != Status.OK.getStatusCode()) {
throw new RuntimeException("Unable to connect to jira repository: " + response.toString());
}
this.projectOptional = getProject();
mappingRegistry = new MappingRegistry();
this.workItemTypeMapping = new WorkItemTypeMapping();
}
public void createOrUpdateItem(int rtcId) throws Exception {
ODocument workItem = StorageQuery.getRTCWorkItem(this.store, rtcId);
createOrUpdateItem(workItem);
}
@Override
public void createOrUpdateItem(ODocument item) throws Exception {
int workItemId = Integer.parseInt(item.field(FieldNames.ID));
ensureWorkItemWithId(workItemId);
Date modified = StorageQuery.getField(item, FieldNames.MODIFIED, Date.from(Instant.now()));
Date lastExport = StorageQuery.getField(item, FieldNames.JIRA_EXPORT_TIMESTAMP, new Date(0));
if (Settings.getInstance().isForceUpdate() || modified.compareTo(lastExport) > 0) {
if (!"SRVS Management".equals(item.field(FieldNames.PROJECT_AREA))
&& !_tempMovedItems.contains(Integer.valueOf(workItemId))) {
updateItem(item);
updatedItems.add(Integer.valueOf(workItemId));
}
}
}
private void ensureWorkItemWithId(int workItemId) throws Exception {
// get current highest id from server, if necessary
if (highestExistingId == -1) {
IssueSearchResult searchResult =
IssueSearch.INSTANCE.run("project = '" + settings.getJiraProjectKey() + "' ORDER BY id DESC");
if (searchResult.getTotal() > 0) {
Issue last = searchResult.getIssues().get(0);
highestExistingId = extractId(last.getKey());
} else {
highestExistingId = 0;
}
}
// set target to total export item count, if necessary
int totalItemsToExport = Settings.getInstance().getTotalItems();
if (totalItemsToExport > 0 && totalItemsToExport > workItemId) {
workItemId = totalItemsToExport;
}
while (highestExistingId < workItemId) {
int gap = workItemId - highestExistingId;
gap = (gap <= 100) ? gap : 100;
createDummyIssues(gap);
}
}
private void createDummyIssues(int total) throws Exception {
if (projectOptional.isPresent()) {
// build request entity
Project project = projectOptional.get();
BulkCreateContainer postEntity = new BulkCreateContainer();
List<BulkCreateEntry> issueUpdates = postEntity.getIssueUpdates();
for (int i = 0; i < total; i++) {
Issue issue = new Issue();
IssueFields fields = issue.getFields();
fields.setProject(project);
fields.setIssuetype(workItemTypeMapping.getIssueType(IssueType.TASK.getName(), project));
fields.setSummary(DUMMY_SUMMARY_TEXT);
fields.setDescription("This is just a dummy issue. Delete it after successfully migrating to Jira.");
issueUpdates.add(new BulkCreateEntry(fields));
}
// post request
long startTime = System.currentTimeMillis();
LOGGER.log(Level.INFO, "Starting bulk creation of " + total + " items.");
ClientResponse postResponse = getRestAccess().post("/issue/bulk", postEntity);
if (postResponse.getStatus() == Status.CREATED.getStatusCode()) {
BulkCreateResponseEntity respEntity = postResponse.getEntity(BulkCreateResponseEntity.class);
List<Issue> issues = respEntity.getIssues();
if (!issues.isEmpty()) {
highestExistingId = extractId(issues.get(issues.size() - 1).getKey());
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
double minutes = Math.floor(duration / (1000 * 60));
double seconds = (duration % (1000 * 60)) / 1000;
LOGGER.log(Level.INFO, "Bulk creation of " + issues.size() + " items took " + (int) minutes + " min. and "
+ (int) seconds + " sec.");
} else {
String errorMessage = "Problems while bulk creating issues: " + postResponse.getEntity(String.class);
throw new Exception(errorMessage);
}
}
}
private void updateItem(ODocument item) throws Exception {
if (projectOptional.isPresent()) {
Project project = projectOptional.get();
Issue issue = createIssueFromWorkItem(item, project);
if (issue != null) {
persistIssue(item, issue);
persistNewComments(item, issue);
try {
persistAttachments(item, issue);
} catch (IOException e) {
throw new Exception("Fatal error - could not open attachment directory while exporting", e);
}
}
}
}
private String getKey(ODocument item) {
String key = null;
if (projectOptional.isPresent()) {
String id = item.field(FieldNames.ID);
key = getIssueKey(id);
}
return key;
}
public String getIssueKey(String rtcId) {
return settings.getJiraProjectKey() + '-' + rtcId;
}
public void updateIfStillDummy(String jiraKey) throws Exception {
int rtcId = extractId(jiraKey);
if (!this.updatedItems.contains(Integer.valueOf(rtcId))) {
Issue issue = new Issue();
issue.setKey(jiraKey);
ClientResponse cr = getRestAccess().get(issue.getSelfPath());
if (cr.getStatus() == 200) {
issue = cr.getEntity(Issue.class);
if (DUMMY_SUMMARY_TEXT.equals(issue.getFields().getSummary())) {
createOrUpdateItem(rtcId);
} else {
updatedItems.add(Integer.valueOf(rtcId));
}
}
}
}
int extractId(String key) {
String[] split = key.split("-");
return Integer.parseInt(split[1]);
}
private void persistIssue(ODocument item, Issue issue) {
Issue lastExportedIssue = getLastExportedInfo(item, issue.getFields().getIssuetype());
try {
updateIssueInJira(issue, lastExportedIssue);
storeReference(issue, item);
cacheInfoOfLastExport(issue, item);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error while updating in Jira", e);
}
}
private String migrateOldStatus(String oldStatusId, IssueType issueType) {
String result = oldStatusId;
if ("10000".equals(oldStatusId)) { // todo
result = "1"; // open
if (IssueType.BUSINESS_NEED.equals(issueType)) {
result = "10103";
}
} else if (IssueType.BUG.equals(issueType)) {
if ("10001".equals(oldStatusId)) { // done
result = "6"; // verified
}
} else if (IssueType.EPIC.equals(issueType)) {
// do nothing
} else if (IssueType.USER_STORY.equals(issueType)) {
// do nothing
} else if (IssueType.STORY.equals(issueType)) {
// do nothing
} else if (IssueType.BUSINESS_NEED.equals(issueType)) {
if ("10001".equals(oldStatusId)) { // done
result = "6";
}
} else if (IssueType.IMPEDIMENT.equals(issueType)) {
if ("10001".equals(oldStatusId)) { // done
result = "5";
}
} else if (IssueType.TASK.equals(issueType)) {
if ("10001".equals(oldStatusId)) { // done
result = "5";
}
} else if (IssueType.SUB_TASK.equals(issueType)) {
if ("10001".equals(oldStatusId)) { // done
result = "5";
}
}
return result;
}
private Issue getLastExportedInfo(ODocument item, IssueType issueType) {
String lastExportedStatus = item.field(FieldNames.JIRA_LAST_EXPORTED_STATUS);
lastExportedStatus = migrateOldStatus(lastExportedStatus, issueType);
Issue lastExportedIssue = new Issue();
// status
lastExportedIssue.getFields().setStatus(IssueStatus.createStartingStatus(issueType));
if (lastExportedStatus != null) {
Optional<StateEnum> stateOpt = StateEnum.forJiraId(lastExportedStatus, issueType);
stateOpt.ifPresent(se -> {
lastExportedIssue.getFields().setStatus(se.getIssueStatus());
});
if (!stateOpt.isPresent()) {
LOGGER.severe("No StateEnum found for last exported status, id = " + lastExportedStatus);
}
}
return lastExportedIssue;
}
private void persistAttachments(ODocument item, Issue issue) throws IOException {
AttachmentStorage storage = new AttachmentStorage();
String id = item.field(FieldNames.ID);
List<Attachment> attachments = storage.readAttachments(Long.parseLong(id));
if (attachments.size() > 0) {
List<String> alreadyExportedAttachments = getAlreadyExportedAttachments(issue);
final FormDataMultiPart multiPart = new FormDataMultiPart();
int newlyAdded = 0;
for (Attachment attachment : attachments) {
// check if already exported
if (!alreadyExportedAttachments.contains(attachment.getPath().getFileName().toString())) {
final File fileToUpload = attachment.getPath().toFile();
if (fileToUpload != null) {
multiPart.bodyPart(new FileDataBodyPart("file", fileToUpload, MediaType.APPLICATION_OCTET_STREAM_TYPE));
newlyAdded++;
}
}
}
if (newlyAdded > 0) {
try {
getRestAccess().postMultiPart(issue.getSelfPath() + "/attachments", multiPart);
} catch (Exception e) {
LOGGER.severe("Could not upload attachments");
}
}
}
}
List<String> getAlreadyExportedAttachments(Issue issue) {
List<String> result = new ArrayList<String>();
List<IssueAttachment> attachment = issue.getFields().getAttachment();
if (attachment != null) {
for (IssueAttachment issueAttachment : attachment) {
result.add(issueAttachment.getFilename());
}
}
return result;
}
private void persistNewComments(ODocument item, Issue issue) {
List<IssueComment> issueComments = issue.getFields().getComment().getComments();
List<Comment> comments = item.field(FieldNames.COMMENTS);
if (comments != null) {
for (Comment comment : comments) {
IssueComment issueComment = IssueComment.createWithIdAndBody(issue, comment.getJiraId(), comment.getComment());
if (comment.getJiraId() == null) {
JiraUser jiraUser = persistUser(comment);
issueComment.setAuthor(jiraUser);
issueComment.setCreated(comment.getDate());
ClientResponse cr = getRestAccess().post(issueComment.getPath(), issueComment);
if (cr.getStatus() == 201) {
IssueComment issueCommentPosted = cr.getEntity(IssueComment.class);
issueComment.setId(issueCommentPosted.getId());
issueCommentPosted.setIssue(issue);
// update document comment
comment.setJiraId(issueComment.getId());
issueComments.add(issueComment);
} else {
String entity = cr.getEntity(String.class);
LOGGER.severe("Could not add comment ****** " + comment.getComment()
+ " ******* to the issue with the key " + issue.getKey() + ". Response entity: " + entity);
}
}
}
// save comments in item because IDs may have been added
store.setFields(item, //
of(FieldNames.COMMENTS, comments));
}
}
private JiraUser persistUser(Comment comment) {
JiraUser jiraUser = JiraUser.createFromComment(comment);
ClientResponse cr = getRestAccess().get(jiraUser.getSelfPath());
if (!isResponseOk(cr)) {
ClientResponse postResponse = getRestAccess().post(jiraUser.getPath(), jiraUser);
if (isResponseOk(postResponse)) {
jiraUser = postResponse.getEntity(JiraUser.class);
}
}
return jiraUser;
}
void storeReference(Issue jiraIssue, ODocument workItem) {
store.setFields(workItem, //
of(FieldNames.JIRA_KEY_LINK, jiraIssue.getKey()), //
of(FieldNames.JIRA_ID_LINK, jiraIssue.getId()));
}
void cacheInfoOfLastExport(Issue jiraIssue, ODocument workItem) {
store.setFields(
workItem, //
of(FieldNames.JIRA_EXPORT_TIMESTAMP,
StorageQuery.getField(workItem, FieldNames.MODIFIED, Date.from(Instant.now()))),
of(FieldNames.JIRA_LAST_EXPORTED_STATUS, jiraIssue.getFields().getStatus().getId()));
}
private Optional<Project> getProject() {
Project projectConfig = new Project();
projectConfig.setKey(settings.getJiraProjectKey());
return Optional.ofNullable(getRestAccess().get(projectConfig.getSelfPath(), Project.class));
}
Issue createIssueInJira(Issue issue) {
ClientResponse postResponse = getRestAccess().post(issue.getPath(), issue);
if (postResponse.getStatus() == Status.CREATED.getStatusCode()) {
return postResponse.getEntity(Issue.class);
} else {
System.err.println("Problems while creating issue: " + postResponse.getEntity(String.class));
return null;
}
}
private void updateIssueInJira(Issue issue, Issue lastExportedIssue) throws Exception {
// prepare status transition
IssueType issueType = issue.getFields().getIssuetype();
StateEnum targetStatus = issue.getFields().getStatus().getStatusEnum(issueType);
StateEnum currentStatus = lastExportedIssue.getFields().getStatus().getStatusEnum(issueType);
List<String> transitionPath = null;
if (!currentStatus.isEditable() && currentStatus == targetStatus) {
transitionPath = currentStatus.forceTransitionPath(targetStatus);
} else {
transitionPath = currentStatus.getTransitionPath(targetStatus);
}
// put issue in editable state
if (!currentStatus.isEditable()) {
String intermediateTransitionId = transitionPath.remove(0);
if (!doTransition(issue, intermediateTransitionId)) {
throw new Exception();
};
}
// send put request
ClientResponse postResponse = getRestAccess().put("/issue/" + issue.getKey(), issue);
if (!isResponseOk(postResponse)) {
LOGGER.severe("Problems while updating issue: " + postResponse.getEntity(String.class));
throw new Exception();
} else {
while (transitionPath.size() > 0) {
String transitionId = transitionPath.remove(0);
if (!StateEnum.NO_TRANSITION.equals(transitionId)) {
if (!doTransition(issue, transitionId)) {
// revert status
issue.getFields().setStatus(lastExportedIssue.getFields().getStatus());
throw new Exception();
}
} else if (targetStatus != currentStatus) {
// revert status
issue.getFields().setStatus(lastExportedIssue.getFields().getStatus());
throw new Exception();
}
}
}
}
private boolean doTransition(Issue issue, String transitionId) {
String entity = "{\"transition\":{\"id\":" + transitionId + "}}";
ClientResponse postResponse =
getRestAccess().post("/issue/" + issue.getKey() + "/transitions?expand=transitions.fields", entity);
if (isResponseOk(postResponse)) {
return true;
} else {
LOGGER.severe("Problems while transitioning issue: " + postResponse.getEntity(String.class));
return false;
}
}
/**
* Returns null if cannot find corresponding jira issue
*
* @param workItem
* @param project
* @return
*/
Issue createIssueFromWorkItem(ODocument workItem, Project project) {
Issue issue = new Issue();
String id = workItem.field(FieldNames.ID);
issue.setId(id);
String key = getKey(workItem);
issue.setKey(key);
ClientResponse cr = getRestAccess().get(issue.getSelfPath());
if (cr.getStatus() == 200) {
issue = cr.getEntity(Issue.class);
IssueFields issueFields = issue.getFields();
String retrievedIssueKey = issue.getKey();
if (!retrievedIssueKey.startsWith("RTC")) {
LOGGER.log(Level.WARNING, "The issue " + key + " has been moved to another project: " + retrievedIssueKey);
issue = null;
} else {
issueFields.setProject(project);
store.setFields(workItem, of(FieldNames.JIRA_LAST_EXPORTED_STATUS, issueFields.getStatus().getId()));
mappingRegistry.map(workItem, issue, store);
// set resolution to appropriate default, otherwise it will be set to "fixed" whenever
// status
// is "done", even if issue is not a defect
if (issueFields.getStatus().getStatusEnum(issue.getFields().getIssuetype()).isFinished()
&& issueFields.getResolution() == null) {
issueFields.setResolution(new IssueResolution(ResolutionEnum.done));
}
}
} else {
String issueKey = issue.getKey();
LOGGER
.log(
Level.SEVERE,
"A problem occurred while retrieving the issue with the key " + issueKey + " : "
+ cr.getEntity(String.class));
issue = null;
}
return issue;
}
private boolean isResponseOk(ClientResponse cr) {
return cr.getStatus() >= Status.OK.getStatusCode() && cr.getStatus() <= Status.PARTIAL_CONTENT.getStatusCode();
}
public JiraRestAccess getRestAccess() {
return restAccess;
}
private void setRestAccess(JiraRestAccess restAccess) {
this.restAccess = restAccess;
}
@Override
public void postExport() throws Exception {}
}