/*
* Copyright 2013 Hewlett-Packard Development Company, L.P
*
* 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
*
* 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.
*/
package com.hp.alm.ali.idea.services;
import com.hp.alm.ali.idea.entity.EntityQuery;
import com.hp.alm.ali.idea.entity.EntityRef;
import com.hp.alm.ali.idea.entity.CachingEntityListener;
import com.hp.alm.ali.idea.rest.RestService;
import com.hp.alm.ali.idea.rest.ServerType;
import com.hp.alm.ali.idea.rest.ServerTypeListener;
import com.hp.alm.ali.idea.model.Entity;
import com.hp.alm.ali.idea.model.parser.EntityList;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
import org.apache.commons.lang.ObjectUtils;
import org.jdom.Element;
import javax.swing.SortOrder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
@State(
name = "SprintService",
storages = { @Storage(id = "default",file = "$WORKSPACE_FILE$") }
)
public class SprintService implements PersistentStateComponent<Element>, ServerTypeListener, CachingEntityListener {
private static SimpleDateFormat ALM_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private EntityService entityService;
private RestService restService;
final private Selector releaseSelector = new Selector();
final private Selector sprintSelector = new Selector();
final private Selector teamSelector = new Selector();
private WeakListeners<Listener> listeners = new WeakListeners<Listener>();
public SprintService(Project project) {
entityService = project.getComponent(EntityService.class);
restService = project.getComponent(RestService.class);
entityService.addEntityListener(this);
restService.addServerTypeListener(this);
}
public Element getState() {
Element element = new Element(getClass().getSimpleName());
addElement(element, releaseSelector.selected);
addElement(element, sprintSelector.selected);
addElement(element, teamSelector.selected);
return element;
}
private void addElement(Element element, Entity entity) {
if(entity != null) {
element.setAttribute(entity.getType()+"-id", String.valueOf(entity.getId()));
element.setAttribute(entity.getType()+"-name", entity.getPropertyValue("name"));
}
}
public void loadState(Element state) {
selectSprint(loadEntity(state, "release-cycle"));
selectTeam(loadEntity(state, "team"));
selectRelease(loadEntity(state, "release"));
}
private Entity loadEntity(Element element, String type) {
String id = element.getAttributeValue(type + "-id");
if(id != null) {
Entity entity = new Entity(type, Integer.valueOf(id));
entity.setProperty("name", element.getAttributeValue(type+"-name"));
return entity;
} else {
return null;
}
}
// method should be considered private except for usage in tests
synchronized void resetValues() {
teamSelector.values = null;
sprintSelector.values = null;
releaseSelector.values = null;
}
@Override
public void connectedTo(ServerType serverType) {
if(ServerType.AGM.equals(restService.getServerTypeIfAvailable())) {
synchronized (this) {
resetValues();
final Entity release = this.releaseSelector.selected;
if(release != null) {
teamSelector.requestRunning = true;
sprintSelector.requestRunning = true;
releaseSelector.requestRunning = true;
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
loadTeams(release);
loadSprints(release);
loadReleases();
}
});
} else {
this.releaseSelector.requestRunning = true;
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
loadReleases();
}
});
}
}
}
}
public synchronized Entity getRelease() {
return releaseSelector.selected;
}
public synchronized Entity getSprint() {
return sprintSelector.selected;
}
public synchronized Entity getTeam() {
return teamSelector.selected;
}
private void loadReleases() {
EntityQuery query = new EntityQuery("release");
query.addColumn("id", 1);
query.addColumn("name", 1);
query.addColumn("start-date", 1);
query.addColumn("end-date", 1);
EntityList list = EntityList.empty();
try {
list = entityService.query(query);
} finally {
synchronized (this) {
releaseSelector.values = list;
releaseSelector.requestRunning = false;
if(!releaseSelector.values.contains(releaseSelector.selected)) {
selectRelease(findClosest(list));
}
notifyAll();
}
}
}
private void loadSprints(final Entity release) {
EntityQuery query = new EntityQuery("release-cycle");
query.addColumn("id", 1);
query.addColumn("name", 1);
query.addColumn("tense", 1);
query.addColumn("start-date", 1);
query.addColumn("end-date", 1);
query.setValue("parent-id", String.valueOf(release.getId()));
query.setPropertyResolved("parent-id", true);
query.addOrder("start-date", SortOrder.ASCENDING);
EntityList list = EntityList.empty();
try {
list = entityService.query(query);
} finally {
synchronized (this) {
sprintSelector.values = list;
sprintSelector.requestRunning = false;
if(!sprintSelector.values.contains(sprintSelector.selected)) {
selectSprint(findClosest(list));
}
notifyAll();
}
}
}
private Entity findClosest(List<Entity> list) {
if(!list.isEmpty()) {
final long now = System.currentTimeMillis();
// sort according to time distance
Collections.sort(list, new Comparator<Entity>() {
@Override
public int compare(Entity entity1, Entity entity2) {
return (int)(distance(now, entity1) - distance(now, entity2));
}
});
return list.get(0);
} else {
return null;
}
}
private void loadTeams(final Entity release) {
EntityQuery query = new EntityQuery("team");
query.addColumn("id", 1);
query.addColumn("name", 1);
// release by cross filter is necessary to support global team
query.setValue("release.id", release.getPropertyValue("id"));
query.setPropertyResolved("release.id", true);
query.addOrder("name", SortOrder.ASCENDING);
EntityList list = EntityList.empty();
try {
list = entityService.query(query);
} finally {
synchronized (this) {
teamSelector.values = list;
teamSelector.requestRunning = false;
if(!teamSelector.values.contains(teamSelector.selected)) {
// TODO: choose my team
selectTeam(list.isEmpty()? null: list.get(0));
}
notifyAll();
}
}
}
private void fireSprintSelected() {
listeners.fire(new WeakListeners.Action<Listener>() {
public void fire(Listener listener) {
listener.onSprintSelected(sprintSelector.selected);
}
});
}
private void fireTeamSelected() {
listeners.fire(new WeakListeners.Action<Listener>() {
public void fire(Listener listener) {
listener.onTeamSelected(teamSelector.selected);
}
});
}
private void fireReleaseSelected() {
listeners.fire(new WeakListeners.Action<Listener>() {
public void fire(Listener listener) {
listener.onReleaseSelected(releaseSelector.selected);
}
});
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public synchronized void selectRelease(final Entity release) {
if((release != null && release.equals(this.releaseSelector.selected)) ||
(release == null && this.releaseSelector.selected == null)) {
return;
}
this.releaseSelector.selected = release;
if(release != null) {
if(ServerType.AGM.equals(restService.getServerTypeIfAvailable())) {
sprintSelector.requestRunning = true;
teamSelector.requestRunning = true;
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
loadSprints(release);
loadTeams(release);
}
});
}
} else {
selectSprint(null);
selectTeam(null);
}
fireReleaseSelected();
}
public synchronized void selectSprint(Entity sprint) {
// if sprint has different tense, we want the event fired (otherwise the inactive sprint warning would not be
// displayed when the original sprint was loaded from persisted configuration)
if((sprint != null && sprint.equals(this.sprintSelector.selected) && isSameTense(sprint, this.sprintSelector.selected)) ||
(sprint == null && this.sprintSelector.selected == null)) {
return;
}
this.sprintSelector.selected = sprint;
fireSprintSelected();
}
public synchronized void selectTeam(Entity team) {
if((team != null && team.equals(this.teamSelector.selected)) ||
(team == null && this.teamSelector.selected == null)) {
return;
}
this.teamSelector.selected = team;
fireTeamSelected();
}
public synchronized Entity getCurrentSprint() {
if(sprintSelector.values == null) {
return null;
}
for(Entity sprint: sprintSelector.values) {
if(isCurrentSprint(sprint)) {
return sprint;
}
}
return null;
}
private synchronized EntityList getValues(final Selector selector, final Runnable loader) {
while(true) {
if(selector.requestRunning) {
// request running
} else if(selector.values != null) {
return selector.values;
} else {
selector.requestRunning = true;
ApplicationManager.getApplication().executeOnPooledThread(loader);
}
try {
wait();
} catch (InterruptedException e) {
}
}
}
public synchronized EntityList getTeams() {
final Entity release = this.releaseSelector.selected;
if(release != null) {
return getValues(teamSelector, new Runnable() {
@Override
public void run() {
loadTeams(release);
}
});
} else {
return null;
}
}
public EntityList getSprints() {
final Entity release = this.releaseSelector.selected;
if(release != null) {
return getValues(sprintSelector, new Runnable() {
@Override
public void run() {
loadSprints(release);
}
});
} else {
return null;
}
}
public EntityList getReleases() {
return getValues(releaseSelector, new Runnable() {
@Override
public void run() {
loadReleases();
}
});
}
public static boolean isSameTense(Entity sprint1, Entity sprint2) {
return ObjectUtils.equals(sprint1.getProperty("tense"), sprint2.getProperty("tense"));
}
public static boolean isCurrentSprint(Entity sprint) {
return "CURRENT".equalsIgnoreCase(sprint.getPropertyValue("tense"));
}
public static long distance(long now, Entity entity) {
try {
Date startDate = ALM_DATE_FORMAT.parse(entity.getPropertyValue("start-date"));
if(now < startDate.getTime()) {
// future release
return startDate.getTime() - now;
}
Date endDate = ALM_DATE_FORMAT.parse(entity.getPropertyValue("end-date"));
// make end-date inclusive
Calendar calendar = Calendar.getInstance();
calendar.setTime(endDate);
calendar.add(Calendar.DATE, 1);
endDate = calendar.getTime();
if(now > endDate.getTime()) {
return now - endDate.getTime();
}
return 0;
} catch(Exception e) {
return Long.MAX_VALUE;
}
}
@Override
public Entity lookup(final EntityRef ref) {
if("release".equals(ref.type)) {
return find(releaseSelector, ref);
} else if("release-cycle".equals(ref.type)) {
return find(sprintSelector, ref);
} else if("team".equals(ref.type)) {
return find(teamSelector, ref);
} else {
return null;
}
}
private synchronized Entity find(Selector selector, EntityRef ref) {
if(selector.values != null) {
int i = selector.values.indexOf(new Entity(ref.type, ref.id));
if(i >= 0) {
return selector.values.get(i);
}
}
return null;
}
@Override
public void entityLoaded(Entity entity, Event event) {
// no need to implement as long as nobody modifies these entity types
}
@Override
public void entityNotFound(EntityRef ref, boolean removed) {
}
public static interface Listener {
void onReleaseSelected(Entity release);
void onSprintSelected(Entity sprint);
void onTeamSelected(Entity team);
}
private static class Selector {
private boolean requestRunning;
private Entity selected;
private EntityList values;
}
}