blob: eafe341d3989a125b149358d827a7b4f2dc6b2ca [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* 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.intellij.tasks.trello;
import com.google.gson.JsonParseException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.tasks.Task;
import com.intellij.tasks.TaskBundle;
import com.intellij.tasks.TaskRepositoryType;
import com.intellij.tasks.impl.BaseRepository;
import com.intellij.tasks.impl.BaseRepositoryImpl;
import com.intellij.tasks.impl.TaskUtil;
import com.intellij.tasks.impl.httpclient.ResponseUtil;
import com.intellij.tasks.trello.model.TrelloBoard;
import com.intellij.tasks.trello.model.TrelloCard;
import com.intellij.tasks.trello.model.TrelloList;
import com.intellij.tasks.trello.model.TrelloUser;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.xmlb.annotations.Tag;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.util.EncodingUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Set;
import static com.intellij.tasks.trello.TrelloUtil.TRELLO_API_BASE_URL;
/**
* @author Mikhail Golubev
*/
@Tag("Trello")
public final class TrelloRepository extends BaseRepositoryImpl {
private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.trello.TrelloRepository");
// User is actually needed only to check ownership of card (by its id)
private TrelloUser myCurrentUser;
private TrelloBoard myCurrentBoard;
private TrelloList myCurrentList;
/**
* Include cards not assigned to current user
*/
private boolean myIncludeAllCards;
/**
* Serialization constructor
*/
@SuppressWarnings("UnusedDeclaration")
public TrelloRepository() {
}
/**
* Normal instantiation constructor
*/
public TrelloRepository(TaskRepositoryType type) {
super(type);
}
/**
* Cloning constructor
*/
public TrelloRepository(TrelloRepository other) {
super(other);
myCurrentUser = other.myCurrentUser;
myCurrentBoard = other.myCurrentBoard;
myCurrentList = other.myCurrentList;
myIncludeAllCards = other.myIncludeAllCards;
}
@Override
public boolean equals(Object o) {
if (!super.equals(o)) return false;
if (o.getClass() != getClass()) return false;
TrelloRepository repository = (TrelloRepository)o;
if (!Comparing.equal(myCurrentUser, repository.myCurrentUser)) return false;
if (!Comparing.equal(myCurrentBoard, repository.myCurrentBoard)) return false;
if (!Comparing.equal(myCurrentList, repository.myCurrentList)) return false;
return myIncludeAllCards == repository.myIncludeAllCards;
}
@NotNull
@Override
public BaseRepository clone() {
return new TrelloRepository(this);
}
@Override
public Task[] getIssues(@Nullable String query, int offset, int limit, boolean withClosed) throws Exception {
List<TrelloCard> cards = fetchCards(offset + limit, withClosed);
return ContainerUtil.map2Array(cards, Task.class, new Function<TrelloCard, Task>() {
@Override
public Task fun(TrelloCard card) {
return new TrelloTask(card, TrelloRepository.this);
}
});
}
@Nullable
@Override
public Task findTask(@NotNull String id) throws Exception {
TrelloCard card = fetchCardById(id);
return card != null ? new TrelloTask(card, this) : null;
}
@Nullable
public TrelloCard fetchCardById(@NotNull String id) throws Exception {
String url = TRELLO_API_BASE_URL + "/cards/" + id + "?actions=commentCard&fields=" + encodeUrl(TrelloCard.REQUIRED_FIELDS);
try {
return makeRequestAndDeserializeJsonResponse(url, TrelloCard.class);
}
// Trello returns string "The requested resource was not found." or "invalid id"
// if card can't be found, which not only cannot be deserialized, but also not valid JSON at all.
catch (JsonParseException e) {
return null;
}
}
@Nullable
public TrelloUser getCurrentUser() {
return myCurrentUser;
}
public void setCurrentUser(TrelloUser currentUser) {
myCurrentUser = currentUser;
}
@Nullable
public TrelloBoard getCurrentBoard() {
return myCurrentBoard;
}
public void setCurrentBoard(TrelloBoard currentBoard) {
myCurrentBoard = currentBoard;
}
@Nullable
public TrelloList getCurrentList() {
return myCurrentList;
}
public void setCurrentList(TrelloList currentList) {
myCurrentList = currentList;
}
/**
* Add authorization token and developer key in any request to Trello
*/
@Override
protected void configureHttpMethod(HttpMethod method) {
if (StringUtil.isEmpty(myPassword)) {
return;
}
String params = EncodingUtil.formUrlEncode(new NameValuePair[]{
new NameValuePair("token", myPassword),
new NameValuePair("key", TrelloRepositoryType.DEVELOPER_KEY)
}, "utf-8");
String oldParams = method.getQueryString();
method.setQueryString(StringUtil.isEmpty(oldParams) ? params : oldParams + "&" + params);
}
@Nullable
@Override
public String extractId(@NotNull String taskName) {
return TrelloUtil.TRELLO_ID_PATTERN.matcher(taskName).matches() ? taskName : null;
}
/**
* Request user information using supplied authorization token
*/
@NotNull
public TrelloUser fetchUserByToken() throws Exception {
try {
String url = TRELLO_API_BASE_URL + "/members/me?fields=" + encodeUrl(TrelloUser.REQUIRED_FIELDS);
return makeRequestAndDeserializeJsonResponse(url, TrelloUser.class);
}
catch (Exception e) {
LOG.warn("Error while fetching initial user info", e);
// invalidate board and list if user can't be found
myCurrentBoard = null;
myCurrentList = null;
throw e;
}
}
@NotNull
public TrelloBoard fetchBoardById(@NotNull String id) throws Exception {
String url = TRELLO_API_BASE_URL + "/boards/" + id + "?fields=" + encodeUrl(TrelloBoard.REQUIRED_FIELDS);
try {
return makeRequestAndDeserializeJsonResponse(url, TrelloBoard.class);
}
catch (Exception e) {
LOG.warn("Error while fetching initial board info", e);
throw e;
}
}
@NotNull
public TrelloList fetchListById(@NotNull String id) throws Exception {
String url = TRELLO_API_BASE_URL + "/lists/" + id + "?fields=" + encodeUrl(TrelloList.REQUIRED_FIELDS);
try {
return makeRequestAndDeserializeJsonResponse(url, TrelloList.class);
}
catch (Exception e) {
LOG.warn("Error while fetching initial list info" + id, e);
throw e;
}
}
@NotNull
public List<TrelloList> fetchBoardLists() throws Exception {
if (myCurrentBoard == null) {
throw new IllegalStateException("Board not set");
}
String url = TRELLO_API_BASE_URL + "/boards/" + myCurrentBoard.getId() + "/lists?fields=" + encodeUrl(TrelloList.REQUIRED_FIELDS);
return makeRequestAndDeserializeJsonResponse(url, TrelloUtil.LIST_OF_LISTS_TYPE);
}
@NotNull
public List<TrelloBoard> fetchUserBoards() throws Exception {
if (myCurrentUser == null) {
throw new IllegalStateException("User not set");
}
String url = TRELLO_API_BASE_URL + "/members/me/boards?filter=open&fields=" + encodeUrl(TrelloBoard.REQUIRED_FIELDS);
return makeRequestAndDeserializeJsonResponse(url, TrelloUtil.LIST_OF_BOARDS_TYPE);
}
@NotNull
public List<TrelloCard> fetchCards(int limit, boolean withClosed) throws Exception {
boolean fromList = false;
// choose most appropriate card provider
String baseUrl;
if (myCurrentList != null) {
baseUrl = TRELLO_API_BASE_URL + "/lists/" + myCurrentList.getId() + "/cards";
fromList = true;
}
else if (myCurrentBoard != null) {
baseUrl = TRELLO_API_BASE_URL + "/boards/" + myCurrentBoard.getId() + "/cards";
}
else if (myCurrentUser != null) {
baseUrl = TRELLO_API_BASE_URL + "/members/me/cards";
}
else {
throw new IllegalStateException("Not configured");
}
String fetchCardsUrl = baseUrl + "?fields=" + encodeUrl(TrelloCard.REQUIRED_FIELDS) + "&limit" + limit;
// 'visible' filter for some reason is not supported for lists
fetchCardsUrl += withClosed || fromList ? "&filter=all" : "&filter=visible";
List<TrelloCard> cards = makeRequestAndDeserializeJsonResponse(fetchCardsUrl, TrelloUtil.LIST_OF_CARDS_TYPE);
LOG.debug("Total " + cards.size() + " cards downloaded");
if (!myIncludeAllCards) {
cards = ContainerUtil.filter(cards, new Condition<TrelloCard>() {
@Override
public boolean value(TrelloCard card) {
return card.getIdMembers().contains(myCurrentUser.getId());
}
});
LOG.debug("Total " + cards.size() + " cards after filtering");
}
if (!cards.isEmpty()) {
if (fromList) {
baseUrl = TRELLO_API_BASE_URL + "/boards/" + cards.get(0).getIdBoard() + "/cards";
}
// fix for IDEA-111470 and IDEA-111475
// Select IDs of visible cards, e.d. cards that either archived explicitly, belong to archived list or closed board.
// This information can't be extracted from single card description, because its 'closed' field
// reflects only the card state and doesn't show state of parental list and board.
// NOTE: According to Trello REST API "filter=visible" parameter may be used only when fetching cards for
// particular board or user.
String visibleCardsUrl = baseUrl + "?filter=visible&fields=none";
List<TrelloCard> visibleCards = makeRequestAndDeserializeJsonResponse(visibleCardsUrl, TrelloUtil.LIST_OF_CARDS_TYPE);
LOG.debug("Total " + visibleCards.size() + " visible cards");
Set<String> visibleCardsIDs = ContainerUtil.map2Set(visibleCards, new Function<TrelloCard, String>() {
@Override
public String fun(TrelloCard card) {
return card.getId();
}
});
for (TrelloCard card : cards) {
card.setVisible(visibleCardsIDs.contains(card.getId()));
}
}
return cards;
}
/**
* Make GET request to specified URL and return HTTP entity of result as Reader object
*/
@NotNull
private String makeRequest(@NotNull String url) throws Exception {
HttpMethod method = new GetMethod(url);
configureHttpMethod(method);
return executeMethod(method);
}
@NotNull
private String executeMethod(@NotNull HttpMethod method) throws Exception {
HttpClient client = getHttpClient();
client.executeMethod(method);
String entityContent = ResponseUtil.getResponseContentAsString(method);
TaskUtil.prettyFormatJsonToLog(LOG, entityContent);
// LOG.debug("Response size: " + method.getResponseHeader("Content-Length").getValue() + " bytes");
if (method.getStatusCode() != HttpStatus.SC_OK) {
Header header = method.getResponseHeader("Content-Type");
if (header != null && header.getValue().startsWith("text/plain")) {
throw new Exception(TaskBundle.message("failure.server.message", StringUtil.capitalize(entityContent)));
}
throw new Exception(TaskBundle.message("failure.http.error", method.getStatusCode(), method.getStatusText()));
}
return entityContent;
}
@NotNull
private <T> T makeRequestAndDeserializeJsonResponse(@NotNull String url, @NotNull Type type) throws Exception {
String entityStream = makeRequest(url);
// javac 1.6.0_23 bug workaround
// TrelloRepository.java:286: type parameters of <T>T cannot be determined; no unique maximal instance exists for type variable T with upper bounds T,java.lang.Object
//noinspection unchecked
return (T)TrelloUtil.GSON.fromJson(entityStream, type);
}
@NotNull
private <T> T makeRequestAndDeserializeJsonResponse(@NotNull String url, @NotNull Class<T> cls) throws Exception {
String entityStream = makeRequest(url);
return TrelloUtil.GSON.fromJson(entityStream, cls);
}
@Override
public String getPresentableName() {
String pseudoUrl = "trello.com";
if (myCurrentBoard != null) {
pseudoUrl += "/" + myCurrentBoard.getName();
}
if (myCurrentList != null) {
pseudoUrl += "/" + myCurrentList.getName();
}
return pseudoUrl;
}
public boolean isIncludeAllCards() {
return myIncludeAllCards;
}
public void setIncludeAllCards(boolean includeAllCards) {
myIncludeAllCards = includeAllCards;
}
@Nullable
@Override
public CancellableConnection createCancellableConnection() {
GetMethod method = new GetMethod(TRELLO_API_BASE_URL + "/members/me/cards?limit=1");
configureHttpMethod(method);
return new HttpTestConnection<GetMethod>(method) {
@Override
protected void doTest(GetMethod method) throws Exception {
executeMethod(method);
}
};
}
@Override
public boolean isConfigured() {
return super.isConfigured() && StringUtil.isNotEmpty(myPassword);
}
@Override
public String getUrl() {
return "trello.com";
}
@Override
protected int getFeatures() {
return super.getFeatures() & ~NATIVE_SEARCH;
}
}