View Javadoc

1   /**
2    * Copyright (C) 2008 Atlassian
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.atlassian.theplugin.commons.crucible.api.rest;
18  
19  import com.atlassian.theplugin.commons.VersionedVirtualFile;
20  import com.atlassian.theplugin.commons.cfg.ServerCfg;
21  import com.atlassian.theplugin.commons.crucible.ProjectCache;
22  import com.atlassian.theplugin.commons.crucible.ValueNotYetInitialized;
23  import com.atlassian.theplugin.commons.crucible.api.CrucibleSession;
24  import com.atlassian.theplugin.commons.crucible.api.model.*;
25  import com.atlassian.theplugin.commons.exception.IncorrectVersionException;
26  import com.atlassian.theplugin.commons.remoteapi.*;
27  import com.atlassian.theplugin.commons.remoteapi.rest.AbstractHttpSession;
28  import com.atlassian.theplugin.commons.remoteapi.rest.HttpSessionCallback;
29  import com.atlassian.theplugin.commons.util.ProductVersionUtil;
30  import org.apache.commons.codec.binary.Base64;
31  import org.apache.commons.httpclient.Header;
32  import org.apache.commons.httpclient.HttpMethod;
33  import org.apache.commons.lang.StringUtils;
34  import org.jdom.Document;
35  import org.jdom.Element;
36  import org.jdom.JDOMException;
37  import org.jdom.xpath.XPath;
38  
39  import java.io.IOException;
40  import java.io.UnsupportedEncodingException;
41  import java.net.MalformedURLException;
42  import java.net.URLEncoder;
43  import java.net.UnknownHostException;
44  import java.util.*;
45  
46  /**
47   * Communication stub for Crucible REST API.
48   */
49  public class CrucibleSessionImpl extends AbstractHttpSession implements CrucibleSession {
50  	private static final String AUTH_SERVICE = "/rest-service/auth-v1";
51  	private static final String REVIEW_SERVICE = "/rest-service/reviews-v1";
52  	private static final String PROJECTS_SERVICE = "/rest-service/projects-v1";
53  	private static final String REPOSITORIES_SERVICE = "/rest-service/repositories-v1";
54  	private static final String USER_SERVICE = "/rest-service/users-v1";
55  
56  	private static final String LOGIN = "/login";
57  	private static final String REVIEWS_IN_STATES = "?state=";
58  	private static final String FILTERED_REVIEWS = "/filter";
59  	private static final String SEARCH_REVIEWS = "/search";
60  	private static final String SEARCH_REVIEWS_QUERY = "?path=";
61  	private static final String DETAIL_REVIEW_INFO = "/details";
62  	private static final String ACTIONS = "/actions";
63  	private static final String TRANSITIONS = "/transitions";
64  	private static final String REVIEWERS = "/reviewers";
65  	private static final String REVIEW_ITEMS = "/reviewitems";
66  	private static final String METRICS = "/metrics";
67  	private static final String VERSION = "/versionInfo";
68  
69  	private static final String COMMENTS = "/comments";
70  	private static final String GENERAL_COMMENTS = "/comments/general";
71  	private static final String VERSIONED_COMMENTS = "/comments/versioned";
72  	private static final String REPLIES = "/replies";
73  
74  	private static final String APPROVE_ACTION = "action:approveReview";
75  	private static final String SUBMIT_ACTION = "action:submitReview";
76  	private static final String SUMMARIZE_ACTION = "action:summarizeReview";
77  	private static final String ABANDON_ACTION = "action:abandonReview";
78  	private static final String CLOSE_ACTION = "action:closeReview";
79  	private static final String RECOVER_ACTION = "action:recoverReview";
80  	private static final String REOPEN_ACTION = "action:reopenReview";
81  	private static final String REJECT_ACTION = "action:rejectReview";
82  	private static final String TRANSITION_ACTION = "/transition?action=";
83  
84  	private static final String PUBLISH_COMMENTS = "/publish";
85  	private static final String COMPLETE_ACTION = "/complete";
86  	private static final String UNCOMPLETE_ACTION = "/uncomplete";
87  
88  	private static final String ADD_CHANGESET = "/addChangeset";
89  	private static final String ADD_PATCH = "/addPatch";
90  //	private static final String ADD_ITEM = "/addItem";
91  
92  	private String authToken;
93  
94  	private Map<String, SvnRepository> repositories = new HashMap<String, SvnRepository>();
95  	private Map<String, List<CustomFieldDef>> metricsDefinitions = new HashMap<String, List<CustomFieldDef>>();
96  	private ProjectCache projectCache;
97  
98  	private CrucibleVersionInfo crucibleVersionInfo;
99  
100 	/**
101 	 * Public constructor for CrucibleSessionImpl.
102 	 *
103 	 * @param serverCfg The server fisheye configuration for this session
104 	 * @param callback  The callback needed for preparing HttpClient calls
105 	 * @throws com.atlassian.theplugin.commons.remoteapi.RemoteApiMalformedUrlException
106 	 *          when serverCfg configuration is invalid
107 	 */
108 	public CrucibleSessionImpl(ServerCfg serverCfg, HttpSessionCallback callback) throws RemoteApiMalformedUrlException {
109 		super(serverCfg, callback);
110 
111 		projectCache = new ProjectCache(this);
112 
113 	}
114 
115 	public void login() throws RemoteApiLoginException {
116 		if (!isLoggedIn()) {
117 			String loginUrl;
118 			try {
119 				if (getUsername() == null || getPassword() == null) {
120 					throw new RemoteApiLoginException("Corrupted configuration. Username or Password null");
121 				}
122 				loginUrl = baseUrl + AUTH_SERVICE + LOGIN + "?userName=" + URLEncoder.encode(getUsername(), "UTF-8")
123 						+ "&password=" + URLEncoder.encode(getPassword(), "UTF-8");
124 			} catch (UnsupportedEncodingException e) {
125 				///CLOVER:OFF
126 				throw new RuntimeException("URLEncoding problem: " + e.getMessage());
127 				///CLOVER:ON
128 			}
129 
130 			try {
131 				Document doc = retrieveGetResponse(loginUrl);
132 				String exception = getExceptionMessages(doc);
133 				if (null != exception) {
134 					throw new RemoteApiLoginFailedException(exception);
135 				}
136 				XPath xpath = XPath.newInstance("/loginResult/token");
137 				List<?> elements = xpath.selectNodes(doc);
138 				if (elements == null) {
139 					throw new RemoteApiLoginException("Server did not return any authentication token");
140 				}
141 				if (elements.size() != 1) {
142 					throw new RemoteApiLoginException("Server returned unexpected number of authentication tokens ("
143 							+ elements.size() + ")");
144 				}
145 				this.authToken = ((Element) elements.get(0)).getText();
146 			} catch (MalformedURLException e) {
147 				throw new RemoteApiLoginException("Malformed server URL: " + baseUrl, e);
148 			} catch (UnknownHostException e) {
149 				throw new RemoteApiLoginException("Unknown host: " + e.getMessage(), e);
150 			} catch (IOException e) {
151 				throw new RemoteApiLoginException(baseUrl + ":" + e.getMessage(), e);
152 			} catch (JDOMException e) {
153 				throw new RemoteApiLoginException("Server:" + baseUrl + " returned malformed response", e);
154 			} catch (RemoteApiSessionExpiredException e) {
155 				// Crucible does not return this exception
156 			} catch (IllegalArgumentException e) {
157 				throw new RemoteApiLoginException("Malformed server URL: " + baseUrl, e);
158 			}
159 		}
160 	}
161 
162 	public void logout() {
163 		if (authToken != null) {
164 			authToken = null;
165 		}
166 	}
167 
168 	public CrucibleVersionInfo getServerVersion() throws RemoteApiException {
169 		if (!isLoggedIn()) {
170 			throw new IllegalStateException("Calling method without calling login() first");
171 		}
172 
173 		String requestUrl = baseUrl + REVIEW_SERVICE + VERSION;
174 		try {
175 			Document doc = retrieveGetResponse(requestUrl);
176 
177 			XPath xpath = XPath.newInstance("versionInfo");
178 			@SuppressWarnings("unchecked")
179 			List<Element> elements = xpath.selectNodes(doc);
180 
181 			if (elements != null && !elements.isEmpty()) {
182 				for (Element element : elements) {
183 					this.crucibleVersionInfo = CrucibleRestXmlHelper.parseVersionNode(element);
184 					return this.crucibleVersionInfo;
185 				}
186 			}
187 
188 			throw new RemoteApiException("No version info found in server response");
189 		} catch (IOException e) {
190 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
191 		} catch (JDOMException e) {
192 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
193 		}
194 	}
195 
196 	private void updateMetricsMetadata(Review review) {
197 		try {
198 			getMetrics(review.getMetricsVersion());
199 		} catch (RemoteApiException e) {
200 			// can be swallowed - metrics metadata are useful, but not necessery
201 		}
202 	}
203 
204 	public List<Review> getReviewsInStates(List<State> states, boolean details) throws RemoteApiException {
205 		if (!isLoggedIn()) {
206 			throw new IllegalStateException("Calling method without calling login() first");
207 		}
208 
209 		StringBuilder sb = new StringBuilder();
210 		sb.append(baseUrl);
211 		sb.append(REVIEW_SERVICE);
212 		if (details) {
213 			sb.append(DETAIL_REVIEW_INFO);
214 		}
215 		if (states != null && states.size() != 0) {
216 			sb.append(REVIEWS_IN_STATES);
217 			for (Iterator<State> stateIterator = states.iterator(); stateIterator.hasNext();) {
218 				State state = stateIterator.next();
219 				sb.append(state.value());
220 				if (stateIterator.hasNext()) {
221 					sb.append(",");
222 				}
223 			}
224 		}
225 
226 		try {
227 			Document doc = retrieveGetResponse(sb.toString());
228 
229 			XPath xpath;
230 			if (details) {
231 				xpath = XPath.newInstance("/detailedReviews/detailedReviewData");
232 			} else {
233 				xpath = XPath.newInstance("/reviews/reviewData");
234 			}
235 			@SuppressWarnings("unchecked")
236 			List<Element> elements = xpath.selectNodes(doc);
237 			List<Review> reviews = new ArrayList<Review>();
238 
239 			if (elements != null && !elements.isEmpty()) {
240 				for (Element element : elements) {
241 					if (details) {
242 						reviews.add(prepareDetailReview(element));
243 					} else {
244 						reviews.add(CrucibleRestXmlHelper.parseReviewNode(baseUrl, element));
245 					}
246 				}
247 			}
248 			for (Review review : reviews) {
249 				updateMetricsMetadata(review);
250 			}
251 			return reviews;
252 		} catch (IOException e) {
253 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
254 		} catch (JDOMException e) {
255 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
256 		}
257 	}
258 
259 	public List<Review> getAllReviews(boolean details) throws RemoteApiException {
260 		return getReviewsInStates(null, details);
261 	}
262 
263 	public List<Review> getReviewsForFilter(PredefinedFilter filter, boolean details) throws RemoteApiException {
264 		if (!isLoggedIn()) {
265 			throw new IllegalStateException("Calling method without calling login() first");
266 		}
267 
268 		try {
269 			String url = baseUrl
270 					+ REVIEW_SERVICE
271 					+ FILTERED_REVIEWS
272 					+ "/" + filter.getFilterUrl();
273 			if (details) {
274 				url += DETAIL_REVIEW_INFO;
275 			}
276 			Document doc = retrieveGetResponse(url);
277 
278 			XPath xpath;
279 			if (details) {
280 				xpath = XPath.newInstance("/detailedReviews/detailedReviewData");
281 			} else {
282 				xpath = XPath.newInstance("/reviews/reviewData");
283 			}
284 			@SuppressWarnings("unchecked")
285 			List<Element> elements = xpath.selectNodes(doc);
286 			List<Review> reviews = new ArrayList<Review>();
287 
288 			if (elements != null && !elements.isEmpty()) {
289 				for (Element element : elements) {
290 					if (details) {
291 						reviews.add(prepareDetailReview(element));
292 					} else {
293 						reviews.add(CrucibleRestXmlHelper.parseReviewNode(baseUrl, element));
294 					}
295 				}
296 			}
297 			for (Review review : reviews) {
298 				updateMetricsMetadata(review);
299 			}
300 			return reviews;
301 		} catch (IOException e) {
302 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
303 		} catch (JDOMException e) {
304 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
305 		}
306 	}
307 
308 	private boolean checkCustomFiltersAsGet() {
309 		if (crucibleVersionInfo == null) {
310 			try {
311 				getServerVersion();
312 			} catch (RemoteApiException e) {
313 				return false;
314 			}
315 		}
316 		try {
317 			ProductVersionUtil version = new ProductVersionUtil(crucibleVersionInfo.getReleaseNumber());
318 			if (version.greater(new ProductVersionUtil("1.6.3"))) {
319 				return true;
320 			}
321 		} catch (IncorrectVersionException e) {
322 			return false;
323 		}
324 		return false;
325 	}
326 
327 
328 	public List<Review> getReviewsForCustomFilter(CustomFilter filter, boolean details) throws RemoteApiException {
329 		if (!isLoggedIn()) {
330 			throw new IllegalStateException("Calling method without calling login() first");
331 		}
332 
333 		try {
334 			Document doc;
335 			if (checkCustomFiltersAsGet()) {
336 				doc = getReviewsForCustomFilterAsGet(filter, details);
337 			} else {
338 				doc = getReviewsForCustomFilterAsPost(filter, details);
339 			}
340 
341 			XPath xpath;
342 			if (details) {
343 				xpath = XPath.newInstance("/detailedReviews/detailedReviewData");
344 			} else {
345 				xpath = XPath.newInstance("/reviews/reviewData");
346 			}
347 			@SuppressWarnings("unchecked")
348 			List<Element> elements = xpath.selectNodes(doc);
349 			List<Review> reviews = new ArrayList<Review>();
350 
351 			if (elements != null && !elements.isEmpty()) {
352 				for (Element element : elements) {
353 					if (details) {
354 						reviews.add(prepareDetailReview(element));
355 					} else {
356 						reviews.add(CrucibleRestXmlHelper.parseReviewNode(baseUrl, element));
357 					}
358 				}
359 			}
360 			for (Review review : reviews) {
361 				updateMetricsMetadata(review);
362 			}
363 			return reviews;
364 		} catch (JDOMException e) {
365 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
366 		}
367 	}
368 
369 	private Document getReviewsForCustomFilterAsPost(CustomFilter filter, boolean details) throws RemoteApiException {
370 		Document request = CrucibleRestXmlHelper.prepareCustomFilter(filter);
371 		try {
372 			String url = baseUrl + REVIEW_SERVICE + FILTERED_REVIEWS;
373 			if (details) {
374 				url += DETAIL_REVIEW_INFO;
375 			}
376 			Document doc = retrievePostResponse(url, request);
377 			return doc;
378 		} catch (IOException e) {
379 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
380 		} catch (JDOMException e) {
381 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
382 		}
383 	}
384 
385 	private Document getReviewsForCustomFilterAsGet(CustomFilter filter, boolean details) throws RemoteApiException {
386 		try {
387 			String url = baseUrl + REVIEW_SERVICE + FILTERED_REVIEWS;
388 			if (details) {
389 				url += DETAIL_REVIEW_INFO;
390 			}
391 			String urlFilter = filter.getFilterUrl();
392 			if (!StringUtils.isEmpty(urlFilter)) {
393 				url += "?" + urlFilter;
394 			}
395 
396 			Document doc = retrieveGetResponse(url);
397 			return doc;
398 		} catch (IOException e) {
399 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
400 		} catch (JDOMException e) {
401 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
402 		}
403 	}
404 
405 	public List<Review> getAllReviewsForFile(String repoName, String path, boolean details) throws RemoteApiException {
406 		if (!isLoggedIn()) {
407 			throw new IllegalStateException("Calling method without calling login() first");
408 		}
409 
410 		try {
411 			String url = baseUrl
412 					+ REVIEW_SERVICE
413 					+ SEARCH_REVIEWS
414 					+ "/"
415 					+ URLEncoder.encode(repoName, "UTF-8");
416 			if (details) {
417 				url += DETAIL_REVIEW_INFO;
418 			}
419 			url = url
420 					+ SEARCH_REVIEWS_QUERY
421 					+ URLEncoder.encode(path, "UTF-8");
422 			Document doc = retrieveGetResponse(url);
423 
424 			XPath xpath;
425 			if (details) {
426 				xpath = XPath.newInstance("/detailedReviews/detailReviewData");
427 			} else {
428 				xpath = XPath.newInstance("/reviews/reviewData");
429 			}
430 			@SuppressWarnings("unchecked")
431 			List<Element> elements = xpath.selectNodes(doc);
432 			List<Review> reviews = new ArrayList<Review>();
433 
434 			if (elements != null && !elements.isEmpty()) {
435 				for (Element element : elements) {
436 					if (details) {
437 						reviews.add(prepareDetailReview(element));
438 					} else {
439 						reviews.add(CrucibleRestXmlHelper.parseReviewNode(baseUrl, element));
440 					}
441 				}
442 			}
443 			for (Review review : reviews) {
444 				updateMetricsMetadata(review);
445 			}
446 			return reviews;
447 		} catch (IOException e) {
448 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
449 		} catch (JDOMException e) {
450 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
451 		}
452 	}
453 
454 	public Review getReview(PermId permId, boolean details) throws RemoteApiException {
455 		if (!isLoggedIn()) {
456 			throw new IllegalStateException("Calling method without calling login() first");
457 		}
458 
459 		try {
460 			String url = baseUrl
461 					+ REVIEW_SERVICE
462 					+ "/"
463 					+ permId.getId();
464 			if (details) {
465 				url += DETAIL_REVIEW_INFO;
466 			}
467 			Document doc = retrieveGetResponse(url);
468 
469 			XPath xpath;
470 			if (details) {
471 				xpath = XPath.newInstance("/detailedReviewData");
472 			} else {
473 				xpath = XPath.newInstance("reviewData");
474 			}
475 			@SuppressWarnings("unchecked")
476 			List<Element> elements = xpath.selectNodes(doc);
477 
478 			if (elements != null && !elements.isEmpty()) {
479 				for (Element element : elements) {
480 					if (details) {
481 						return prepareDetailReview(element);
482 					} else {
483 						return CrucibleRestXmlHelper.parseReviewNode(baseUrl, element);
484 					}
485 				}
486 			}
487 			return null;
488 		} catch (IOException e) {
489 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
490 		} catch (JDOMException e) {
491 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
492 		}
493 	}
494 
495 	public void fillRepositoryData(CrucibleFileInfo fileInfo) throws RemoteApiException {
496 		String repoName = fileInfo.getRepositoryName();
497 		if (repoName == null) {
498 			// oh well, it can be null - fileInfos are mostly empty now
499 			return;
500 		}
501 
502 		String[] repoNameTokens = repoName.split(":");
503 
504 		if (!repositories.containsKey(repoName)) {
505 			SvnRepository repository = getRepository(repoNameTokens.length > 1 ? repoNameTokens[1] : repoNameTokens[0]);
506 			repositories.put(repoName, repository);
507 		}
508 		SvnRepository repository = repositories.get(repoName);
509 		if (repository != null) {
510 			String repoPath = repository.getUrl() + "/" + repository.getPath() + "/";
511 			VersionedVirtualFile oldDescriptor = fileInfo.getOldFileDescriptor();
512 			if (!oldDescriptor.getUrl().equals("")) {
513 				oldDescriptor.setRepoUrl(repoPath);
514 			}
515 			VersionedVirtualFile newDescriptor = fileInfo.getFileDescriptor();
516 			if (!newDescriptor.getUrl().equals("")) {
517 				newDescriptor.setRepoUrl(repoPath);
518 			}
519 		}
520 	}
521 
522 	private ReviewBean prepareDetailReview(Element element) throws RemoteApiException {
523 		ReviewBean review = CrucibleRestXmlHelper.parseDetailedReviewNode(baseUrl, getUsername(), element);
524 
525 
526 		review.setProject(projectCache.getProject(review.getProjectKey()));
527 
528 //		for (CrucibleFileInfo fileInfo : CrucibleFileInfoManager.getInstance().getFiles(review)) {
529 //			fillRepositoryData(fileInfo);
530 //		}
531 		try {
532 			for (CrucibleFileInfo fileInfo : review.getFiles()) {
533 				fillRepositoryData(fileInfo);
534 			}
535 		} catch (ValueNotYetInitialized valueNotYetInitialized) {
536 			// cannot fill
537 		}
538 		return review;
539 	}
540 
541 	public List<Reviewer> getReviewers(PermId permId) throws RemoteApiException {
542 		if (!isLoggedIn()) {
543 			throw new IllegalStateException("Calling method without calling login() first");
544 		}
545 
546 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + REVIEWERS;
547 		try {
548 			Document doc = retrieveGetResponse(requestUrl);
549 
550 			XPath xpath = XPath.newInstance("/reviewers/reviewer");
551 			@SuppressWarnings("unchecked")
552 			List<Element> elements = xpath.selectNodes(doc);
553 			List<Reviewer> reviewers = new ArrayList<Reviewer>();
554 
555 			if (elements != null && !elements.isEmpty()) {
556 				for (Element element : elements) {
557 					reviewers.add(CrucibleRestXmlHelper.parseReviewerNode(element));
558 				}
559 			}
560 			return reviewers;
561 		} catch (IOException e) {
562 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
563 		} catch (JDOMException e) {
564 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
565 		}
566 	}
567 
568 	public List<User> getUsers() throws RemoteApiException {
569 		if (!isLoggedIn()) {
570 			throw new IllegalStateException("Calling method without calling login() first");
571 		}
572 
573 		String requestUrl = baseUrl + USER_SERVICE;
574 		try {
575 			Document doc = retrieveGetResponse(requestUrl);
576 
577 			XPath xpath = XPath.newInstance("/users/userData");
578 			@SuppressWarnings("unchecked")
579 			List<Element> elements = xpath.selectNodes(doc);
580 			List<User> users = new ArrayList<User>();
581 
582 			if (elements != null && !elements.isEmpty()) {
583 				for (Element element : elements) {
584 					// bug PL-1002: sometimes we get empty user name
585 					UserBean u = CrucibleRestXmlHelper.parseUserNode(element);
586 					if (u.getDisplayName().equals("")) {
587 						u.setDisplayName(u.getUserName());
588 					}
589 					users.add(u);
590 				}
591 			}
592 			return users;
593 		} catch (IOException e) {
594 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
595 		} catch (JDOMException e) {
596 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
597 		}
598 	}
599 
600 	/**
601 	 * Retrieves projects from cache (reduces server calls)
602 	 *
603 	 * @return list of Crucible Projects
604 	 * @throws RemoteApiException thrown in case of connection problems
605 	 */
606 	public List<CrucibleProject> getProjectsFromCache() throws RemoteApiException {
607 		return projectCache.getProjects();
608 	}
609 
610 	/**
611 	 * Retrieves projects directly from server ommiting cache
612 	 *
613 	 * @return list of Crucible projects
614 	 * @throws RemoteApiException thrown in case of connection problems
615 	 * @deprecated {@link #getProjectsFromCache()} should be used
616 	 */
617 	public List<CrucibleProject> getProjects() throws RemoteApiException {
618 		return getProjectsFromServer();
619 	}
620 
621 	private List<CrucibleProject> getProjectsFromServer() throws RemoteApiException {
622 		if (!isLoggedIn()) {
623 			throw new IllegalStateException("Calling method without calling login() first");
624 		}
625 
626 		String requestUrl = baseUrl + PROJECTS_SERVICE;
627 		try {
628 			Document doc = retrieveGetResponse(requestUrl);
629 
630 			XPath xpath = XPath.newInstance("/projects/projectData");
631 			@SuppressWarnings("unchecked")
632 			List<Element> elements = xpath.selectNodes(doc);
633 			List<CrucibleProject> projects = new ArrayList<CrucibleProject>();
634 
635 			if (elements != null && !elements.isEmpty()) {
636 				for (Element element : elements) {
637 					projects.add(CrucibleRestXmlHelper.parseProjectNode(element));
638 				}
639 			}
640 			return projects;
641 		} catch (IOException e) {
642 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
643 		} catch (JDOMException e) {
644 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
645 		}
646 	}
647 
648 	public List<Repository> getRepositories() throws RemoteApiException {
649 		if (!isLoggedIn()) {
650 			throw new IllegalStateException("Calling method without calling login() first");
651 		}
652 
653 		String requestUrl = baseUrl + REPOSITORIES_SERVICE;
654 		try {
655 			Document doc = retrieveGetResponse(requestUrl);
656 
657 			XPath xpath = XPath.newInstance("/repositories/repoData");
658 			@SuppressWarnings("unchecked")
659 			List<Element> elements = xpath.selectNodes(doc);
660 			List<Repository> myRepositories = new ArrayList<Repository>();
661 
662 			if (elements != null && !elements.isEmpty()) {
663 				for (Element element : elements) {
664 					myRepositories.add(CrucibleRestXmlHelper.parseRepositoryNode(element));
665 				}
666 			}
667 			return myRepositories;
668 		} catch (IOException e) {
669 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
670 		} catch (JDOMException e) {
671 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
672 		}
673 	}
674 
675 	public SvnRepository getRepository(String repoName) throws RemoteApiException {
676 		if (!isLoggedIn()) {
677 			throw new IllegalStateException("Calling method without calling login() first");
678 		}
679 
680 		List<Repository> myRepositories = getRepositories();
681 		for (Repository repository : myRepositories) {
682 			if (repository.getName().equals(repoName)) {
683 				if (repository.getType().equals("svn")) {
684 					String requestUrl = baseUrl + REPOSITORIES_SERVICE + "/" + repoName + "/svn";
685 					try {
686 						Document doc = retrieveGetResponse(requestUrl);
687 						XPath xpath = XPath.newInstance("/svnRepositoryData");
688 						@SuppressWarnings("unchecked")
689 						List<Element> elements = xpath.selectNodes(doc);
690 						if (elements != null && !elements.isEmpty()) {
691 							for (Element element : elements) {
692 								return CrucibleRestXmlHelper.parseSvnRepositoryNode(element);
693 							}
694 						}
695 					} catch (IOException e) {
696 						throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
697 					} catch (JDOMException e) {
698 						throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
699 					}
700 				}
701 			}
702 		}
703 		return null;
704 	}
705 
706 	public Set<CrucibleFileInfo> getFiles(PermId id) throws RemoteApiException {
707 		if (!isLoggedIn()) {
708 			throw new IllegalStateException("Calling method without calling login() first");
709 		}
710 
711 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + REVIEW_ITEMS;
712 		try {
713 			Document doc = retrieveGetResponse(requestUrl);
714 
715 			XPath xpath = XPath.newInstance("reviewItems/reviewItem");
716 			@SuppressWarnings("unchecked")
717 			List<Element> elements = xpath.selectNodes(doc);
718 			Set<CrucibleFileInfo> reviewItems = new HashSet<CrucibleFileInfo>();
719 
720 			Review changeSet = new ReviewBean(baseUrl);
721 			if (elements != null && !elements.isEmpty()) {
722 				for (Element element : elements) {
723 					CrucibleFileInfo fileInfo = CrucibleRestXmlHelper.parseReviewItemNode(changeSet, element);
724 					fillRepositoryData(fileInfo);
725 					reviewItems.add(fileInfo);
726 				}
727 			}
728 			return reviewItems;
729 		} catch (IOException e) {
730 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
731 		} catch (JDOMException e) {
732 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
733 		}
734 	}
735 
736 //	public CrucibleFileInfo addItemToReview(Review review, NewReviewItem item) throws RemoteApiException {
737 //		if (!isLoggedIn()) {
738 //			throw new IllegalStateException("Calling method without calling login() first");
739 //		}
740 //
741 //		Document request = CrucibleRestXmlHelper.prepareAddItemNode(item);
742 //		try {
743 //			String url = baseUrl + REVIEW_SERVICE + "/" + review.getPermId().getId() + REVIEW_ITEMS;
744 //			Document doc = retrievePostResponse(url, request);
745 //			XPath xpath = XPath.newInstance("/reviewItem");
746 //			@SuppressWarnings("unchecked")
747 //			List<Element> elements = xpath.selectNodes(doc);
748 //
749 //			if (elements != null && !elements.isEmpty()) {
750 //				CrucibleFileInfo fileInfo = CrucibleRestXmlHelper.parseReviewItemNode(review, elements.iterator().next());
751 //				fillRepositoryData(fileInfo);
752 //				CrucibleFileInfoManager.getInstance().getFiles(review).add(fileInfo);
753 //				return fileInfo;
754 //			}
755 //			return null;
756 //		} catch (IOException e) {
757 //			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
758 //		} catch (JDOMException e) {
759 //			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
760 //		}
761 //	}
762 
763 	public List<GeneralComment> getGeneralComments(PermId id) throws RemoteApiException {
764 		if (!isLoggedIn()) {
765 			throw new IllegalStateException("Calling method without calling login() first");
766 		}
767 
768 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + GENERAL_COMMENTS;
769 		try {
770 			Document doc = retrieveGetResponse(requestUrl);
771 
772 			XPath xpath = XPath.newInstance("comments/generalCommentData");
773 			@SuppressWarnings("unchecked")
774 			List<Element> elements = xpath.selectNodes(doc);
775 			List<GeneralComment> comments = new ArrayList<GeneralComment>();
776 
777 			if (elements != null && !elements.isEmpty()) {
778 				for (Element element : elements) {
779 					GeneralComment c = CrucibleRestXmlHelper.parseGeneralCommentNode(getUsername(), element);
780 					if (c != null) {
781 						comments.add(c);
782 					}
783 				}
784 			}
785 			return comments;
786 		} catch (IOException e) {
787 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
788 		} catch (JDOMException e) {
789 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
790 		}
791 	}
792 
793 	public List<VersionedComment> getAllVersionedComments(PermId id) throws RemoteApiException {
794 		if (!isLoggedIn()) {
795 			throw new IllegalStateException("Calling method without calling login() first");
796 		}
797 
798 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + VERSIONED_COMMENTS;
799 		try {
800 			Document doc = retrieveGetResponse(requestUrl);
801 
802 			XPath xpath = XPath.newInstance("comments/versionedLineCommentData");
803 			@SuppressWarnings("unchecked")
804 			List<Element> elements = xpath.selectNodes(doc);
805 			List<VersionedComment> comments = new ArrayList<VersionedComment>();
806 
807 			if (elements != null && !elements.isEmpty()) {
808 				for (Element element : elements) {
809 					VersionedComment c = CrucibleRestXmlHelper.parseVersionedCommentNode(getUsername(), element);
810 					if (c != null) {
811 						comments.add(c);
812 					}
813 				}
814 			}
815 			return comments;
816 		} catch (IOException e) {
817 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
818 		} catch (JDOMException e) {
819 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
820 		}
821 	}
822 
823 	public List<VersionedComment> getVersionedComments(PermId id, PermId reviewItemId) throws RemoteApiException {
824 		if (!isLoggedIn()) {
825 			throw new IllegalStateException("Calling method without calling login() first");
826 		}
827 
828 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + REVIEW_ITEMS + "/" + reviewItemId.getId() + COMMENTS;
829 		try {
830 			Document doc = retrieveGetResponse(requestUrl);
831 
832 			XPath xpath = XPath.newInstance("comments/versionedLineCommentData");
833 			@SuppressWarnings("unchecked")
834 			List<Element> elements = xpath.selectNodes(doc);
835 			List<VersionedComment> comments = new ArrayList<VersionedComment>();
836 
837 			if (elements != null && !elements.isEmpty()) {
838 				for (Element element : elements) {
839 					VersionedComment c = CrucibleRestXmlHelper.parseVersionedCommentNode(getUsername(), element);
840 					if (c != null) {
841 						comments.add(c);
842 					}
843 				}
844 			}
845 			return comments;
846 		} catch (IOException e) {
847 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
848 		} catch (JDOMException e) {
849 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
850 		}
851 	}
852 
853 	public List<GeneralComment> getReplies(PermId id, PermId commentId) throws RemoteApiException {
854 		if (!isLoggedIn()) {
855 			throw new IllegalStateException("Calling method without calling login() first");
856 		}
857 
858 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS + "/" + commentId.getId() + REPLIES;
859 		try {
860 			Document doc = retrieveGetResponse(requestUrl);
861 
862 			XPath xpath = XPath.newInstance("comments/generalCommentData");
863 			@SuppressWarnings("unchecked")
864 			List<Element> elements = xpath.selectNodes(doc);
865 			List<GeneralComment> comments = new ArrayList<GeneralComment>();
866 
867 			if (elements != null && !elements.isEmpty()) {
868 				for (Element element : elements) {
869 					GeneralComment c = CrucibleRestXmlHelper.parseGeneralCommentNode(getUsername(), element);
870 					if (c != null) {
871 						comments.add(c);
872 					}
873 				}
874 			}
875 			return comments;
876 		} catch (IOException e) {
877 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
878 		} catch (JDOMException e) {
879 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
880 		}
881 	}
882 
883 //	public List<Comment> getComments(PermId id) throws RemoteApiException {
884 //		if (!isLoggedIn()) {
885 //			throw new IllegalStateException("Calling method without calling login() first");
886 //		}
887 //
888 //		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS;
889 //		try {
890 //			Document doc = retrieveGetResponse(requestUrl);
891 //
892 //			XPath xpath = XPath.newInstance("comments/generalCommentData");
893 //			@SuppressWarnings("unchecked")
894 //			List<Element> elements = xpath.selectNodes(doc);
895 //			List<Comment> comments = new ArrayList<Comment>();
896 //
897 //			if (elements != null && !elements.isEmpty()) {
898 //				int i = 1;
899 //				for (Element element : elements) {
900 //					GeneralCommentBean comment = CrucibleRestXmlHelper.parseGeneralCommentNode(element);
901 //					XPath repliesPath = XPath.newInstance("comments/generalCommentData[" + (i++)
902 //							+ "]/replies/generalCommentData");
903 //					@SuppressWarnings("unchecked")
904 //					final List<Element> replies = repliesPath.selectNodes(doc);
905 //					if (replies != null && !replies.isEmpty()) {
906 //						for (Element reply : replies) {
907 //							comment.addReply(CrucibleRestXmlHelper.parseGeneralCommentNode(reply));
908 //						}
909 //					}
910 //					comments.add(comment);
911 //				}
912 //			}
913 //
914 //			xpath = XPath.newInstance("comments/versionedLineCommentData");
915 //			@SuppressWarnings("unchecked")
916 //			List<Element> vElements = xpath.selectNodes(doc);
917 //
918 //			if (vElements != null && !vElements.isEmpty()) {
919 //				int i = 1;
920 //				for (Element element : vElements) {
921 //					VersionedCommentBean comment = CrucibleRestXmlHelper.parseVersionedCommentNode(element);
922 //					XPath repliesPath = XPath.newInstance("comments/versionedLineCommentData[" + (i++)
923 //							+ "]/replies/generalCommentData");
924 //					@SuppressWarnings("unchecked")
925 //					final List<Element> replies = repliesPath.selectNodes(doc);
926 //					if (replies != null && !replies.isEmpty()) {
927 //						for (Element reply : replies) {
928 //							comment.addReply(CrucibleRestXmlHelper.parseVersionedCommentNode(reply));
929 //						}
930 //					}
931 //					comments.add(comment);
932 //				}
933 //			}
934 //
935 //			return comments;
936 //		} catch (IOException e) {
937 //			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
938 //		} catch (JDOMException e) {
939 //			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
940 //		}
941 //	}
942 
943 	public GeneralComment addGeneralComment(PermId id, GeneralComment comment) throws RemoteApiException {
944 		if (!isLoggedIn()) {
945 			throw new IllegalStateException("Calling method without calling login() first");
946 		}
947 
948 		Document request = CrucibleRestXmlHelper.prepareGeneralComment(comment);
949 
950 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS;
951 		try {
952 			Document doc = retrievePostResponse(requestUrl, request);
953 
954 			XPath xpath = XPath.newInstance("generalCommentData");
955 			@SuppressWarnings("unchecked")
956 			List<Element> elements = xpath.selectNodes(doc);
957 
958 			if (elements != null && !elements.isEmpty()) {
959 				for (Element element : elements) {
960 					return CrucibleRestXmlHelper.parseGeneralCommentNode(getUsername(), element);
961 				}
962 			}
963 			return null;
964 		} catch (IOException e) {
965 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
966 		} catch (JDOMException e) {
967 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
968 		}
969 	}
970 
971 	public void removeComment(PermId id, Comment comment) throws RemoteApiException {
972 		if (!isLoggedIn()) {
973 			throw new IllegalStateException("Calling method without calling login() first");
974 		}
975 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS + "/" + comment.getPermId().getId();
976 		try {
977 			retrieveDeleteResponse(requestUrl, false);
978 		} catch (IOException e) {
979 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
980 		} catch (JDOMException e) {
981 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
982 		}
983 	}
984 
985 	public void updateComment(PermId id, Comment comment) throws RemoteApiException {
986 		if (!isLoggedIn()) {
987 			throw new IllegalStateException("Calling method without calling login() first");
988 		}
989 
990 		Document request = CrucibleRestXmlHelper.prepareGeneralComment(comment);
991 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS + "/" + comment.getPermId().getId();
992 
993 		try {
994 			retrievePostResponse(requestUrl, request, false);
995 		} catch (IOException e) {
996 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
997 		} catch (JDOMException e) {
998 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
999 		}
1000 	}
1001 
1002 	public void publishComment(PermId reviewId, PermId commentId) throws RemoteApiException {
1003 		if (!isLoggedIn()) {
1004 			throw new IllegalStateException("Calling method without calling login() first");
1005 		}
1006 
1007 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + reviewId.getId() + PUBLISH_COMMENTS;
1008 		if (commentId != null) {
1009 			requestUrl += "/" + commentId.getId();
1010 		}
1011 
1012 		try {
1013 			retrievePostResponse(requestUrl, "", false);
1014 		} catch (IOException e) {
1015 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1016 		} catch (JDOMException e) {
1017 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1018 		} catch (RemoteApiSessionExpiredException e) {
1019 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1020 		}
1021 	}
1022 
1023 	public VersionedComment addVersionedComment(PermId id, PermId riId, VersionedComment comment) throws RemoteApiException {
1024 		if (!isLoggedIn()) {
1025 			throw new IllegalStateException("Calling method without calling login() first");
1026 		}
1027 
1028 		Document request = CrucibleRestXmlHelper.prepareVersionedComment(riId, comment);
1029 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + REVIEW_ITEMS + "/"
1030 				+ riId.getId() + COMMENTS;
1031 		try {
1032 			Document doc = retrievePostResponse(requestUrl, request);
1033 			XPath xpath = XPath.newInstance("versionedLineCommentData");
1034 			@SuppressWarnings("unchecked")
1035 			List<Element> elements = xpath.selectNodes(doc);
1036 
1037 			if (elements != null && !elements.isEmpty()) {
1038 				for (Element element : elements) {
1039 					return CrucibleRestXmlHelper.parseVersionedCommentNode(getUsername(), element);
1040 				}
1041 			}
1042 			return null;
1043 		} catch (IOException e) {
1044 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1045 		} catch (JDOMException e) {
1046 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1047 		}
1048 	}
1049 
1050 	public GeneralComment addGeneralCommentReply(PermId id, PermId cId, GeneralComment comment) throws RemoteApiException {
1051 		if (!isLoggedIn()) {
1052 			throw new IllegalStateException("Calling method without calling login() first");
1053 		}
1054 
1055 		Document request = CrucibleRestXmlHelper.prepareGeneralComment(comment);
1056 
1057 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS + "/" + cId.getId() + REPLIES;
1058 
1059 		try {
1060 			Document doc = retrievePostResponse(requestUrl, request);
1061 
1062 			XPath xpath = XPath.newInstance("generalCommentData");
1063 			@SuppressWarnings("unchecked")
1064 			List<Element> elements = xpath.selectNodes(doc);
1065 
1066 			if (elements != null && !elements.isEmpty()) {
1067 				for (Element element : elements) {
1068 					GeneralCommentBean reply = CrucibleRestXmlHelper.parseGeneralCommentNode(getUsername(), element);
1069 					if (reply != null) {
1070 						reply.setReply(true);
1071 					}
1072 					return reply;
1073 				}
1074 			}
1075 			return null;
1076 		} catch (IOException e) {
1077 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1078 		} catch (JDOMException e) {
1079 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1080 		}
1081 	}
1082 
1083 	public VersionedComment addVersionedCommentReply(PermId id, PermId cId, VersionedComment comment)
1084 			throws RemoteApiException {
1085 		if (!isLoggedIn()) {
1086 			throw new IllegalStateException("Calling method without calling login() first");
1087 		}
1088 
1089 		Document request = CrucibleRestXmlHelper.prepareGeneralComment(comment);
1090 
1091 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS + "/" + cId.getId() + REPLIES;
1092 
1093 		try {
1094 			Document doc = retrievePostResponse(requestUrl, request);
1095 
1096 			XPath xpath = XPath.newInstance("generalCommentData");  // todo lguminski we should change it to reflect model
1097 			@SuppressWarnings("unchecked")
1098 			List<Element> elements = xpath.selectNodes(doc);
1099 
1100 			if (elements != null && !elements.isEmpty()) {
1101 				for (Element element : elements) {
1102 					VersionedCommentBean reply = CrucibleRestXmlHelper.parseVersionedCommentNode(getUsername(), element);
1103 					if (reply != null) {
1104 						reply.setReply(true);
1105 					}
1106 					return reply;
1107 				}
1108 			}
1109 			return null;
1110 		} catch (IOException e) {
1111 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1112 		} catch (JDOMException e) {
1113 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1114 		}
1115 
1116 	}
1117 
1118 	public void updateReply(PermId id, PermId cId, PermId rId, GeneralComment comment) throws RemoteApiException {
1119 		if (!isLoggedIn()) {
1120 			throw new IllegalStateException("Calling method without calling login() first");
1121 		}
1122 
1123 		Document request = CrucibleRestXmlHelper.prepareGeneralComment(comment);
1124 
1125 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + id.getId() + COMMENTS + "/"
1126 				+ cId.getId() + REPLIES + "/" + rId.getId();
1127 
1128 		try {
1129 			retrievePostResponse(requestUrl, request, false);
1130 		} catch (IOException e) {
1131 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1132 		} catch (JDOMException e) {
1133 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1134 		}
1135 	}
1136 
1137 	public Review createReview(Review review) throws RemoteApiException {
1138 		if (!isLoggedIn()) {
1139 			throw new IllegalStateException("Calling method without calling login() first");
1140 		}
1141 		return createReviewFromPatch(review, null);
1142 	}
1143 
1144 	public Review createReviewFromPatch(Review review, String patch) throws RemoteApiException {
1145 		if (!isLoggedIn()) {
1146 			throw new IllegalStateException("Calling method without calling login() first");
1147 		}
1148 
1149 		Document request = CrucibleRestXmlHelper.prepareCreateReviewNode(review, patch);
1150 		try {
1151 			Document doc = retrievePostResponse(baseUrl + REVIEW_SERVICE, request);
1152 
1153 			XPath xpath = XPath.newInstance("/reviewData");
1154 			@SuppressWarnings("unchecked")
1155 			List<Element> elements = xpath.selectNodes(doc);
1156 
1157 			if (elements != null && !elements.isEmpty()) {
1158 				return CrucibleRestXmlHelper.parseReviewNode(baseUrl, elements.iterator().next());
1159 			}
1160 			return null;
1161 		} catch (IOException e) {
1162 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1163 		} catch (JDOMException e) {
1164 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1165 		}
1166 	}
1167 
1168 	public Review createReviewFromRevision(Review review, List<String> revisions) throws RemoteApiException {
1169 		if (!isLoggedIn()) {
1170 			throw new IllegalStateException("Calling method without calling login() first");
1171 		}
1172 
1173 		Document request = CrucibleRestXmlHelper.prepareCreateReviewNode(review, revisions);
1174 
1175 //		XmlUtil.printXml(request);
1176 
1177 		try {
1178 			Document doc = retrievePostResponse(baseUrl + REVIEW_SERVICE, request);
1179 			XPath xpath = XPath.newInstance("/reviewData");
1180 			@SuppressWarnings("unchecked")
1181 			List<Element> elements = xpath.selectNodes(doc);
1182 
1183 			if (elements != null && !elements.isEmpty()) {
1184 				return CrucibleRestXmlHelper.parseReviewNode(baseUrl, elements.iterator().next());
1185 			}
1186 			return null;
1187 		} catch (IOException e) {
1188 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1189 		} catch (JDOMException e) {
1190 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1191 		}
1192 	}
1193 
1194 	public List<Action> getAvailableActions(PermId permId) throws RemoteApiException {
1195 		if (!isLoggedIn()) {
1196 			throw new IllegalStateException("Calling method without calling login() first");
1197 		}
1198 
1199 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + ACTIONS;
1200 		try {
1201 			Document doc = retrieveGetResponse(requestUrl);
1202 
1203 			XPath xpath = XPath.newInstance("/actions/actionData");
1204 			@SuppressWarnings("unchecked")
1205 			List<Element> elements = xpath.selectNodes(doc);
1206 			List<Action> actions = new ArrayList<Action>();
1207 
1208 			if (elements != null && !elements.isEmpty()) {
1209 				for (Element element : elements) {
1210 					actions.add(CrucibleRestXmlHelper.parseActionNode(element));
1211 				}
1212 			}
1213 			return actions;
1214 		} catch (IOException e) {
1215 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1216 		} catch (JDOMException e) {
1217 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1218 		}
1219 	}
1220 
1221 	public List<Action> getAvailableTransitions(PermId permId) throws RemoteApiException {
1222 		if (!isLoggedIn()) {
1223 			throw new IllegalStateException("Calling method without calling login() first");
1224 		}
1225 
1226 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + TRANSITIONS;
1227 		try {
1228 			Document doc = retrieveGetResponse(requestUrl);
1229 
1230 			XPath xpath = XPath.newInstance("/transitions/actionData");
1231 			@SuppressWarnings("unchecked")
1232 			List<Element> elements = xpath.selectNodes(doc);
1233 			List<Action> transitions = new ArrayList<Action>();
1234 
1235 			if (elements != null && !elements.isEmpty()) {
1236 				for (Element element : elements) {
1237 					transitions.add(CrucibleRestXmlHelper.parseActionNode(element));
1238 				}
1239 			}
1240 			return transitions;
1241 		} catch (IOException e) {
1242 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1243 		} catch (JDOMException e) {
1244 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1245 		}
1246 	}
1247 
1248 	public Review addRevisionsToReview(PermId permId, String repository, List<String> revisions) throws RemoteApiException {
1249 		if (!isLoggedIn()) {
1250 			throw new IllegalStateException("Calling method without calling login() first");
1251 		}
1252 
1253 		Document request = CrucibleRestXmlHelper.prepareAddChangesetNode(repository, revisions);
1254 
1255 		try {
1256 			String url = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + ADD_CHANGESET;
1257 			Document doc = retrievePostResponse(url, request);
1258 
1259 
1260 			XPath xpath = XPath.newInstance("/reviewData");
1261 			@SuppressWarnings("unchecked")
1262 			List<Element> elements = xpath.selectNodes(doc);
1263 
1264 			if (elements != null && !elements.isEmpty()) {
1265 				return CrucibleRestXmlHelper.parseReviewNode(baseUrl, elements.iterator().next());
1266 			}
1267 			return null;
1268 		} catch (IOException e) {
1269 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1270 		} catch (JDOMException e) {
1271 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1272 		}
1273 	}
1274 
1275 	public Review addPatchToReview(PermId permId, String repository, String patch) throws RemoteApiException {
1276 		if (!isLoggedIn()) {
1277 			throw new IllegalStateException("Calling method without calling login() first");
1278 		}
1279 
1280 		Document request = CrucibleRestXmlHelper.prepareAddPatchNode(repository, patch);
1281 
1282 		try {
1283 			String url = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + ADD_PATCH;
1284 			Document doc = retrievePostResponse(url, request);
1285 
1286 			XPath xpath = XPath.newInstance("/reviewData");
1287 			@SuppressWarnings("unchecked")
1288 			List<Element> elements = xpath.selectNodes(doc);
1289 
1290 			if (elements != null && !elements.isEmpty()) {
1291 				return CrucibleRestXmlHelper.parseReviewNode(baseUrl, elements.iterator().next());
1292 			}
1293 			return null;
1294 		} catch (IOException e) {
1295 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1296 		} catch (JDOMException e) {
1297 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1298 		}
1299 	}
1300 
1301 	public void addReviewers(PermId permId, Set<String> users) throws RemoteApiException {
1302 		if (!isLoggedIn()) {
1303 			throw new IllegalStateException("Calling method without calling login() first");
1304 		}
1305 
1306 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + REVIEWERS;
1307 		String reviewers = "";
1308 		for (String user : users) {
1309 			if (reviewers.length() > 0) {
1310 				reviewers += ",";
1311 			}
1312 			reviewers += user;
1313 		}
1314 
1315 		try {
1316 			retrievePostResponse(requestUrl, reviewers, false);
1317 		} catch (IOException e) {
1318 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1319 		} catch (JDOMException e) {
1320 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1321 		}
1322 	}
1323 
1324 	public void removeReviewer(PermId permId, String username) throws RemoteApiException {
1325 		if (!isLoggedIn()) {
1326 			throw new IllegalStateException("Calling method without calling login() first");
1327 		}
1328 
1329 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + REVIEWERS + "/" + username;
1330 		try {
1331 			retrieveDeleteResponse(requestUrl, false);
1332 		} catch (IOException e) {
1333 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1334 		} catch (JDOMException e) {
1335 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1336 		}
1337 	}
1338 
1339 	private Review changeReviewState(PermId permId, String action) throws RemoteApiException {
1340 		if (!isLoggedIn()) {
1341 			throw new IllegalStateException("Calling method without calling login() first");
1342 		}
1343 
1344 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + TRANSITION_ACTION + action;
1345 		try {
1346 			Document doc = retrievePostResponse(requestUrl, "", true);
1347 
1348 			XPath xpath = XPath.newInstance("reviewData");
1349 			@SuppressWarnings("unchecked")
1350 			List<Element> elements = xpath.selectNodes(doc);
1351 			Review review = null;
1352 
1353 			if (elements != null && !elements.isEmpty()) {
1354 				for (Element element : elements) {
1355 					review = CrucibleRestXmlHelper.parseReviewNode(baseUrl, element);
1356 				}
1357 			}
1358 			return review;
1359 		} catch (IOException e) {
1360 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1361 		} catch (JDOMException e) {
1362 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1363 		}
1364 	}
1365 
1366 	public void completeReview(PermId permId, boolean complete) throws RemoteApiException {
1367 		if (!isLoggedIn()) {
1368 			throw new IllegalStateException("Calling method without calling login() first");
1369 		}
1370 
1371 		String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId();
1372 		if (complete) {
1373 			requestUrl += COMPLETE_ACTION;
1374 		} else {
1375 			requestUrl += UNCOMPLETE_ACTION;
1376 		}
1377 
1378 		try {
1379 			retrievePostResponse(requestUrl, "", false);
1380 		} catch (IOException e) {
1381 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1382 		} catch (JDOMException e) {
1383 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1384 		}
1385 	}
1386 
1387 	public Review approveReview(PermId permId) throws RemoteApiException {
1388 		return changeReviewState(permId, APPROVE_ACTION);
1389 	}
1390 
1391 	public Review submitReview(PermId permId) throws RemoteApiException {
1392 		return changeReviewState(permId, SUBMIT_ACTION);
1393 	}
1394 
1395 	public Review abandonReview(PermId permId) throws RemoteApiException {
1396 		return changeReviewState(permId, ABANDON_ACTION);
1397 	}
1398 
1399 	public Review summarizeReview(PermId permId) throws RemoteApiException {
1400 		return changeReviewState(permId, SUMMARIZE_ACTION);
1401 	}
1402 
1403 	public Review recoverReview(PermId permId) throws RemoteApiException {
1404 		return changeReviewState(permId, RECOVER_ACTION);
1405 	}
1406 
1407 	public Review reopenReview(PermId permId) throws RemoteApiException {
1408 		return changeReviewState(permId, REOPEN_ACTION);
1409 	}
1410 
1411 	public Review rejectReview(PermId permId) throws RemoteApiException {
1412 		return changeReviewState(permId, REJECT_ACTION);
1413 	}
1414 
1415 	public Review closeReview(PermId permId, String summarizeMessage) throws RemoteApiException {
1416 		if (!isLoggedIn()) {
1417 			throw new IllegalStateException("Calling method without calling login() first");
1418 		}
1419 
1420 		try {
1421 			Document doc;
1422 			if (summarizeMessage != null && !"".equals(summarizeMessage)) {
1423 				Document request = CrucibleRestXmlHelper.prepareCloseReviewSummaryNode(summarizeMessage);
1424 				String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + "/close";
1425 				doc = retrievePostResponse(requestUrl, request);
1426 			} else {
1427 				String requestUrl = baseUrl + REVIEW_SERVICE + "/" + permId.getId() + TRANSITION_ACTION + CLOSE_ACTION;
1428 				doc = retrievePostResponse(requestUrl, "", true);
1429 			}
1430 
1431 			XPath xpath = XPath.newInstance("reviewData");
1432 			@SuppressWarnings("unchecked")
1433 			List<Element> elements = xpath.selectNodes(doc);
1434 			Review review = null;
1435 
1436 			if (elements != null && !elements.isEmpty()) {
1437 				for (Element element : elements) {
1438 					review = CrucibleRestXmlHelper.parseReviewNode(baseUrl, element);
1439 				}
1440 			}
1441 			return review;
1442 		} catch (IOException e) {
1443 			throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1444 		} catch (JDOMException e) {
1445 			throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1446 		}
1447 	}
1448 
1449 	public List<CustomFieldDef> getMetrics(int version) throws RemoteApiException {
1450 		if (!isLoggedIn()) {
1451 			throw new IllegalStateException("Calling method without calling login() first");
1452 		}
1453 
1454 		String key = Integer.toString(version);
1455 		if (!metricsDefinitions.containsKey(key)) {
1456 			String requestUrl = baseUrl + REVIEW_SERVICE + METRICS + "/" + Integer.toString(version);
1457 			try {
1458 				Document doc = retrieveGetResponse(requestUrl);
1459 
1460 				XPath xpath = XPath.newInstance("metrics/metricsData");
1461 				@SuppressWarnings("unchecked")
1462 				List<Element> elements = xpath.selectNodes(doc);
1463 				List<CustomFieldDef> metrics = new ArrayList<CustomFieldDef>();
1464 
1465 				if (elements != null && !elements.isEmpty()) {
1466 					for (Element element : elements) {
1467 						metrics.add(CrucibleRestXmlHelper.parseMetricsNode(element));
1468 					}
1469 				}
1470 				metricsDefinitions.put(key, metrics);
1471 			} catch (IOException e) {
1472 				throw new RemoteApiException(baseUrl + ": " + e.getMessage(), e);
1473 			} catch (JDOMException e) {
1474 				throw new RemoteApiException(baseUrl + ": Server returned malformed response", e);
1475 			}
1476 		}
1477 		return metricsDefinitions.get(key);
1478 	}
1479 
1480 	@Override
1481 	protected void adjustHttpHeader(HttpMethod method) {
1482 		method.addRequestHeader(new Header("Authorization", getAuthHeaderValue()));
1483 	}
1484 
1485 
1486 	@Override
1487 	protected void preprocessResult(Document doc) throws JDOMException, RemoteApiSessionExpiredException {
1488 
1489 	}
1490 
1491 	private String getAuthHeaderValue() {
1492 		return "Basic " + encode(getUsername() + ":" + getPassword());
1493 	}
1494 
1495 	private synchronized String encode(String str2encode) {
1496 		try {
1497 			Base64 base64 = new Base64();
1498 			byte[] bytes = base64.encode(str2encode.getBytes("UTF-8"));
1499 			return new String(bytes);
1500 		} catch (UnsupportedEncodingException e) {
1501 			throw new RuntimeException("UTF-8 is not supported", e);
1502 		}
1503 	}
1504 
1505 	private static String getExceptionMessages(Document doc) throws JDOMException {
1506 		XPath xpath = XPath.newInstance("/loginResult/error");
1507 		@SuppressWarnings("unchecked")
1508 		List<Element> elements = xpath.selectNodes(doc);
1509 
1510 		if (elements != null && elements.size() > 0) {
1511 			StringBuffer exceptionMsg = new StringBuffer();
1512 			for (Element e : elements) {
1513 				exceptionMsg.append(e.getText());
1514 				exceptionMsg.append("\n");
1515 			}
1516 			return exceptionMsg.toString();
1517 		} else {
1518 			/* no exception */
1519 			return null;
1520 		}
1521 	}
1522 
1523 	public boolean isLoggedIn() {
1524 		return authToken != null;
1525 	}
1526 }