blob: 035955ccbb27c0cad9b8e5b599454a2d09a4189a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 The Eclipse Foundation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* The Eclipse Foundation - initial API and implementation
* Yatta Solutions - bug 397004, bug 385936, bug 432803: public API
*******************************************************************************/
package org.eclipse.epp.internal.mpc.core.service;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.epp.internal.mpc.core.util.HttpUtil;
import org.eclipse.epp.internal.mpc.core.util.ServiceUtil;
import org.eclipse.epp.mpc.core.model.ICategory;
import org.eclipse.epp.mpc.core.model.IIdentifiable;
import org.eclipse.epp.mpc.core.model.IMarket;
import org.eclipse.epp.mpc.core.model.INode;
import org.eclipse.epp.mpc.core.service.IMarketplaceService;
import org.eclipse.osgi.util.NLS;
/**
* @author David Green
* @author Carsten Reckord
*/
@SuppressWarnings("deprecation")
public class DefaultMarketplaceService extends RemoteMarketplaceService<Marketplace> implements IMarketplaceService,
MarketplaceService {
// This provisional API will be identified by /api/p at the end of most urls.
//
// /api/p - Returns Markets + Categories
// /node/%/api/p OR /content/%/api/p - Returns a single listing's detail
// /taxonomy/term/%/api/p - Returns a category listing of results
// /featured/api/p - Returns a server-defined number of featured results.
// /recent/api/p - Returns a server-defined number of recent updates
// /favorites/top/api/p - Returns a server-defined number of top favorites
// /popular/top/api/p - Returns a server-defined number of most active results
// /related/api/p - Returns a server-defined number of recommendations based on a list of nodes provided as query parameter
// /news/api/p - Returns the news configuration details (news location/title...).
//
// There is one exception to adding /api/p at the end and that is for search results.
//
// /api/p/search/apachesolr_search/[query]?page=[]&filters=[] - Returns search result from the Solr Search DB.
//
// Once we've locked down the provisional API it will likely be named api/1.
public static final String API_FAVORITES_URI = "favorites/top"; //$NON-NLS-1$
public static final String API_FEATURED_URI = "featured"; //$NON-NLS-1$
public static final String API_NEWS_URI = "news"; //$NON-NLS-1$
public static final String API_NODE_CONTENT_URI = "content"; //$NON-NLS-1$
public static final String API_NODE_URI = "node"; //$NON-NLS-1$
public static final String API_POPULAR_URI = "popular/top"; //$NON-NLS-1$
public static final String API_RELATED_URI = "related"; //$NON-NLS-1$
public static final String API_RECENT_URI = "recent"; //$NON-NLS-1$
public static final String API_SEARCH_URI = "search/apachesolr_search/"; //$NON-NLS-1$
public static final String API_SEARCH_URI_FULL = API_URI_SUFFIX + '/' + API_SEARCH_URI;
public static final String API_TAXONOMY_URI = "taxonomy/term/"; //$NON-NLS-1$
public static final String DEFAULT_SERVICE_LOCATION = System.getProperty(MarketplaceService.class.getName()
+ ".url", "http://marketplace.eclipse.org"); //$NON-NLS-1$//$NON-NLS-2$
public static final URL DEFAULT_SERVICE_URL;
/**
* parameter identifying client
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_CLIENT = "client"; //$NON-NLS-1$
/**
* parameter identifying windowing system as reported by {@link org.eclipse.core.runtime.Platform#getWS()}
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_WS = "ws"; //$NON-NLS-1$
/**
* parameter identifying operating system as reported by {@link org.eclipse.core.runtime.Platform#getOS()}
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_OS = "os"; //$NON-NLS-1$
/**
* parameter identifying the current local as reported by {@link org.eclipse.core.runtime.Platform#getNL()}
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_NL = "nl"; //$NON-NLS-1$
/**
* parameter identifying Java version
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_JAVA_VERSION = "java.version"; //$NON-NLS-1$
/**
* parameter identifying the Eclipse runtime version (the version of the org.eclipse.core.runtime bundle)
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_RUNTIME_VERSION = "runtime.version"; //$NON-NLS-1$
/**
* parameter identifying the Eclipse platform version (the version of the org.eclipse.platform bundle) This
* parameter is optional and only sent if the platform bundle is present. It is used to identify the actual running
* platform's version (esp. where different platforms share the same runtime, like the parallel 3.x/4.x versions)
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_PLATFORM_VERSION = "platform.version"; //$NON-NLS-1$
/**
* parameter identifying the Eclipse product version
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_PRODUCT_VERSION = "product.version"; //$NON-NLS-1$
/**
* parameter identifying the product id, as provided by <code>Platform.getProduct().getId()</code>
*
* @see {@link #setRequestMetaParameters(Map)}
*/
public static final String META_PARAM_PRODUCT = "product"; //$NON-NLS-1$
/**
* parameter identifying a list of nodes for a {@link #related(List, IProgressMonitor)} query
*/
public static final String PARAM_BASED_ON_NODES = "nodes"; //$NON-NLS-1$
static {
DEFAULT_SERVICE_URL = ServiceUtil.parseUrl(DEFAULT_SERVICE_LOCATION);
}
public DefaultMarketplaceService(URL baseUrl) {
this.baseUrl = baseUrl == null ? DEFAULT_SERVICE_URL : baseUrl;
}
public DefaultMarketplaceService() {
this(null);
}
public List<Market> listMarkets(IProgressMonitor monitor) throws CoreException {
Marketplace marketplace = processRequest(API_URI_SUFFIX, monitor);
return marketplace.getMarket();
}
public Market getMarket(IMarket market, IProgressMonitor monitor) throws CoreException {
if (market.getId() == null && market.getUrl() != null) {
throw new IllegalArgumentException();
}
List<Market> markets = listMarkets(monitor);
if (market.getId() != null) {
String marketId = market.getId();
for (Market aMarket : markets) {
if (marketId.equals(aMarket.getId())) {
return aMarket;
}
}
} else if (market.getUrl() != null) {
String marketUrl = market.getUrl();
for (Market aMarket : markets) {
if (marketUrl.equals(aMarket.getUrl())) {
return aMarket;
}
}
}
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_marketNotFound, null));
}
public Market getMarket(Market market, IProgressMonitor monitor) throws CoreException {
return getMarket((IMarket) market, monitor);
}
public Category getCategory(ICategory category, IProgressMonitor monitor) throws CoreException {
if (category.getId() != null && category.getUrl() == null) {
SubMonitor progress = SubMonitor.convert(monitor, 100);
List<Market> markets = listMarkets(progress.newChild(50));
ICategory resolvedCategory = null;
outer: for (Market market : markets) {
List<Category> categories = market.getCategory();
for (Category aCategory : categories) {
if (aCategory.equalsId(category)) {
resolvedCategory = aCategory;
break outer;
}
}
}
if (progress.isCanceled()) {
throw new OperationCanceledException();
} else if (resolvedCategory == null) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_categoryNotFound, null));
} else {
return getCategory(resolvedCategory, progress.newChild(50));
}
}
Marketplace marketplace = processRequest(category.getUrl(), API_URI_SUFFIX, monitor);
if (marketplace.getCategory().isEmpty()) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_categoryNotFound, null));
} else if (marketplace.getCategory().size() > 1) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_unexpectedResponse, null));
}
return marketplace.getCategory().get(0);
}
public Category getCategory(Category category, IProgressMonitor monitor) throws CoreException {
return getCategory((ICategory) category, monitor);
}
public Node getNode(INode node, IProgressMonitor monitor) throws CoreException {
Marketplace marketplace;
if (node.getId() != null) {
// bug 304928: prefer the id method rather than the URL, since the id provides a stable URL and the
// URL is based on the name, which could change.
String encodedId = urlEncode(node.getId());
marketplace = processRequest(API_NODE_URI + '/' + encodedId + '/' + API_URI_SUFFIX, monitor);
} else {
marketplace = processRequest(node.getUrl(), API_URI_SUFFIX, monitor);
}
if (marketplace.getNode().isEmpty()) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_nodeNotFound, null));
} else if (marketplace.getNode().size() > 1) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_unexpectedResponse, null));
}
return marketplace.getNode().get(0);
}
public Node getNode(Node node, IProgressMonitor monitor) throws CoreException {
return getNode((INode) node, monitor);
}
public SearchResult search(IMarket market, ICategory category, String queryText, IProgressMonitor monitor)
throws CoreException {
SearchResult result = new SearchResult();
String relativeUrl = computeRelativeSearchUrl(market, category, queryText, true);
if (relativeUrl == null) {
// empty search
result.setMatchCount(0);
result.setNodes(new ArrayList<Node>());
} else {
Marketplace marketplace;
try {
marketplace = processRequest(relativeUrl, monitor);
} catch (CoreException ex) {
Throwable cause = ex.getCause();
if (cause instanceof FileNotFoundException) {
throw new CoreException(createErrorStatus(
NLS.bind(Messages.DefaultMarketplaceService_UnsupportedSearchString, queryText), cause));
}
throw ex;
}
Search search = marketplace.getSearch();
if (search == null) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_unexpectedResponse, null));
}
result.setMatchCount(search.getCount());
result.setNodes(search.getNode());
}
return result;
}
public SearchResult search(Market market, Category category, String queryText, IProgressMonitor monitor)
throws CoreException {
return search((IMarket) market, (ICategory) category, queryText, monitor);
}
/**
* Creates the query URL for the Marketplace REST API.
* <p>
* If the query string is non-empty, the format for the returned relative URL is
* <code>search/apachesolr_search/[query]?filters=[filters]</code> where [query] is the URL encoded query string and
* [filters] are the category and market IDs (category first for browser urls, market first for API urls). If both
* market and category are null, the filters are omitted completely.
* <p>
* If the query is empty and either market or category are not null, the format for the relative URL is
* <code>taxonomy/term/[filters]</code> where [filters] is the comma-separated list of category and market, in that
* order.
* <p>
* If the query is empty and both category and market are null, the result is null
*
* @param market
* the market to search or null
* @param category
* the category to search or null
* @param queryText
* the search query
* @param api
* true to create REST API url, false for browser url
* @return the relative search url, e.g.
* <code>api/p/search/apachesolr_search/WikiText?filters=tid:38%20tid:31</code> or
* <code>taxonomy/term/38,31/api/p</code>
*/
public String computeRelativeSearchUrl(IMarket market, ICategory category, String queryText, boolean api) {
String relativeUrl;
if (queryText != null && queryText.trim().length() > 0) {
relativeUrl = (api ? API_SEARCH_URI_FULL : API_SEARCH_URI) + urlEncode(queryText.trim());
String queryString = ""; //$NON-NLS-1$
if (market != null || category != null) {
queryString += "filters="; //$NON-NLS-1$
IIdentifiable first = api ? market : category;
IIdentifiable second = api ? category : market;
if (first != null) {
queryString += "tid:" + urlEncode(first.getId()); //$NON-NLS-1$
if (second != null) {
queryString += "%20"; //$NON-NLS-1$
}
}
if (second != null) {
queryString += "tid:" + urlEncode(second.getId()); //$NON-NLS-1$
}
}
if (queryString.length() > 0) {
relativeUrl += '?' + queryString;
}
} else if (market != null || category != null) {
// http://marketplace.eclipse.org/taxonomy/term/38,31
relativeUrl = API_TAXONOMY_URI;
if (category != null) {
relativeUrl += urlEncode(category.getId());
if (market != null) {
relativeUrl += ',';
}
}
if (market != null) {
relativeUrl += urlEncode(market.getId());
}
if (api) {
relativeUrl += '/' + API_URI_SUFFIX;
}
} else {
relativeUrl = null;
}
return relativeUrl;
}
public SearchResult featured(IProgressMonitor monitor) throws CoreException {
return featured(null, null, monitor);
}
public SearchResult featured(IMarket market, ICategory category, IProgressMonitor monitor) throws CoreException {
String nodePart = ""; //$NON-NLS-1$
if (market != null) {
nodePart += urlEncode(market.getId());
}
if (category != null) {
if (nodePart.length() > 0) {
nodePart += ","; //$NON-NLS-1$
}
nodePart += urlEncode(category.getId());
}
String uri = API_FEATURED_URI + '/';
if (nodePart.length() > 0) {
uri += nodePart + '/';
}
Marketplace marketplace = processRequest(uri + API_URI_SUFFIX, monitor);
return createSearchResult(marketplace.getFeatured());
}
public SearchResult featured(IProgressMonitor monitor, Market market, Category category) throws CoreException {
return featured(market, category, monitor);
}
public SearchResult recent(IProgressMonitor monitor) throws CoreException {
Marketplace marketplace = processRequest(API_RECENT_URI + '/' + API_URI_SUFFIX, monitor);
return createSearchResult(marketplace.getRecent());
}
public SearchResult favorites(IProgressMonitor monitor) throws CoreException {
Marketplace marketplace = processRequest(API_FAVORITES_URI + '/' + API_URI_SUFFIX, monitor);
return createSearchResult(marketplace.getFavorites());
}
public SearchResult popular(IProgressMonitor monitor) throws CoreException {
Marketplace marketplace = processRequest(API_POPULAR_URI + '/' + API_URI_SUFFIX, monitor);
return createSearchResult(marketplace.getPopular());
}
public SearchResult related(List<INode> basedOn, IProgressMonitor monitor) throws CoreException {
String basedOnQuery = ""; //$NON-NLS-1$
if (basedOn != null && !basedOn.isEmpty()) {
StringBuilder sb = new StringBuilder().append('?').append(PARAM_BASED_ON_NODES).append('=');
boolean first = true;
for (INode node : basedOn) {
if (!first) {
sb.append('+');
}
sb.append(node.getId());
first = false;
}
basedOnQuery = sb.toString();
}
Marketplace marketplace = processRequest(API_RELATED_URI + '/' + API_URI_SUFFIX + basedOnQuery, monitor);
return createSearchResult(marketplace.getRelated());
}
protected SearchResult createSearchResult(NodeListing nodeList) throws CoreException {
if (nodeList == null) {
throw new CoreException(createErrorStatus(Messages.DefaultMarketplaceService_unexpectedResponse, null));
}
SearchResult result = new SearchResult();
result.setMatchCount(nodeList.getCount());
result.setNodes(nodeList.getNode());
return result;
}
public News news(IProgressMonitor monitor) throws CoreException {
try {
Marketplace marketplace = processRequest(API_NEWS_URI + '/' + API_URI_SUFFIX, monitor);
return marketplace.getNews();
} catch (CoreException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof FileNotFoundException) {
// optional news API not supported
return null;
}
throw ex;
}
}
/**
* @deprecated use {@link #reportInstallError(IStatus, Set, Set, String, IProgressMonitor)} instead
*/
@Deprecated
public void reportInstallError(IProgressMonitor monitor, IStatus result, Set<Node> nodes,
Set<String> iuIdsAndVersions, String resolutionDetails) throws CoreException {
reportInstallError(result, nodes, iuIdsAndVersions, resolutionDetails, monitor);
}
public void reportInstallError(IStatus result, Set<? extends INode> nodes, Set<String> iuIdsAndVersions,
String resolutionDetails, IProgressMonitor monitor) throws CoreException {
HttpClient client;
URL location;
HttpPost method;
try {
location = new URL(baseUrl, "install/error/report"); //$NON-NLS-1$
String target = location.toURI().toString();
client = HttpUtil.createHttpClient(target);
method = new HttpPost(target);
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
} catch (MalformedURLException e) {
throw new IllegalStateException(e);
}
try {
List<NameValuePair> parameters = new ArrayList<NameValuePair>();
Map<String, String> requestMetaParameters = getRequestMetaParameters();
for (Map.Entry<String, String> metaParam : requestMetaParameters.entrySet()) {
if (metaParam.getKey() != null) {
parameters.add(new BasicNameValuePair(metaParam.getKey(), metaParam.getValue()));
}
}
parameters.add(new BasicNameValuePair("status", Integer.toString(result.getSeverity()))); //$NON-NLS-1$
parameters.add(new BasicNameValuePair("statusMessage", result.getMessage())); //$NON-NLS-1$
for (INode node : nodes) {
parameters.add(new BasicNameValuePair("node", node.getId())); //$NON-NLS-1$
}
if (iuIdsAndVersions != null && !iuIdsAndVersions.isEmpty()) {
for (String iuAndVersion : iuIdsAndVersions) {
parameters.add(new BasicNameValuePair("iu", iuAndVersion)); //$NON-NLS-1$
}
}
parameters.add(new BasicNameValuePair("detailedMessage", resolutionDetails)); //$NON-NLS-1$
if (!parameters.isEmpty()) {
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(parameters, "UTF-8"); //$NON-NLS-1$
method.setEntity(entity);
client.execute(method);
}
} catch (IOException e) {
String message = NLS.bind(Messages.DefaultMarketplaceService_cannotCompleteRequest_reason,
location.toString(), e.getMessage());
throw new CoreException(createErrorStatus(message, e));
} finally {
client.getConnectionManager().shutdown();
}
}
public void reportInstallSuccess(INode node, IProgressMonitor monitor) {
String url = node.getUrl();
if (!url.endsWith("/")) { //$NON-NLS-1$
url += "/"; //$NON-NLS-1$
}
url += "success"; //$NON-NLS-1$
url = addMetaParameters(url);
try {
InputStream stream = transport.stream(new URI(url), monitor);
try {
while (stream.read() != -1) {
// nothing to do
}
} finally {
stream.close();
}
} catch (Throwable e) {
//per bug 314028 logging this error is not useful.
}
}
}