blob: eb0ce84ff2f44fdfff3ce86d568b8c81b84bd9fd [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
*******************************************************************************/
package org.eclipse.epp.internal.mpc.core.service;
import java.io.ByteArrayOutputStream;
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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.fluent.Request;
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.transport.httpclient.RequestTemplate;
import org.eclipse.epp.mpc.core.model.INode;
import org.eclipse.epp.mpc.core.service.IUserFavoritesService;
import org.eclipse.epp.mpc.core.service.QueryHelper;
import org.eclipse.userstorage.IBlob;
import org.eclipse.userstorage.internal.Session;
import org.eclipse.userstorage.internal.util.IOUtil;
import org.eclipse.userstorage.internal.util.StringUtil;
import org.eclipse.userstorage.util.ConflictException;
import org.eclipse.userstorage.util.NoServiceException;
import org.eclipse.userstorage.util.NotFoundException;
import org.eclipse.userstorage.util.ProtocolException;
@SuppressWarnings("restriction")
public class UserFavoritesService extends AbstractDataStorageService implements IUserFavoritesService {
private static final int MALFORMED_CONTENT_ERROR_CODE = 499;
private static final Pattern JSON_CONTENT_ID_PATTERN = Pattern
.compile("\\{[^\\}]*\"content_id\"\\s*:\\s*\"([^\"]*)\"[^\\}]*\\}"); //$NON-NLS-1$
private static final Pattern JSON_MPC_FAVORITES_PATTERN = Pattern
.compile("\\{\\s*\"mpc_favorites\"\\s*:\\s*\\[((?:\\s*\\{[^\\{\\}]+\\}\\s*,?\\s*)*)\\],.*\\}"); //$NON-NLS-1$
private static final Pattern USER_MAIL_PATTERN = Pattern.compile(".+\\@.+"); //$NON-NLS-1$
private static final String FAVORITES_API__ENDPOINT = "/api/mpc_favorites?{0}={1}"; //$NON-NLS-1$
private static final String FAVORITES_API__USER_MAIL = "mail"; //$NON-NLS-1$
private static final String FAVORITES_API__USER_NAME = "name"; //$NON-NLS-1$
private static final String KEY = "mpc_favorites"; //$NON-NLS-1$
private static final int RETRY_COUNT = 3;
private static final String SEPARATOR = ","; //$NON-NLS-1$
private final Map<String, Integer> favoritesCorrections = new HashMap<String, Integer>();
private final Set<String> favorites = new HashSet<String>();
protected IBlob getFavoritesBlob() {
return getStorageService().getBlob(KEY);
}
public Integer getFavoriteCount(INode node) {
Integer favorited = node.getFavorited();
if (favorited == null) {
return null;
}
Integer correction = favoritesCorrections.get(node.getId());
if (correction != null) {
favorited += correction;
}
return favorited;
}
public Set<String> getLastFavoriteIds() {
return Collections.unmodifiableSet(favorites);
}
public Set<String> getFavoriteIds(IProgressMonitor monitor)
throws NoServiceException, NotAuthorizedException, IllegalStateException, IOException {
SubMonitor progress = SubMonitor.convert(monitor, Messages.DefaultMarketplaceService_FavoritesRetrieve, 1000);
try {
String favoritesData = getFavoritesBlob().getContentsUTF();
progress.worked(950);//FIXME waiting for USS bug 488335 to have proper progress and cancelation
Set<String> result = parseFavoritesBlobData(favoritesData);
synchronized (this) {
favorites.clear();
favorites.addAll(result);
}
return result;
} catch (NotFoundException ex) {
//the user does not yet have favorites
return new LinkedHashSet<String>();
} catch (OperationCanceledException ex) {
throw processProtocolException(ex);
} catch (ProtocolException ex) {
throw processProtocolException(ex);
}
}
public List<INode> getLastFavorites() {
Set<String> favoriteIds = getLastFavoriteIds();
List<INode> favoriteNodes = toNodes(favoriteIds);
return favoriteNodes;
}
public List<INode> getFavorites(IProgressMonitor monitor)
throws NoServiceException, NotAuthorizedException, IllegalStateException, IOException {
Set<String> favoriteIds = getFavoriteIds(monitor);
List<INode> favoriteNodes = toNodes(favoriteIds);
return favoriteNodes;
}
private static List<INode> toNodes(Collection<String> favoriteIds) {
List<INode> favoriteNodes = new ArrayList<INode>(favoriteIds.size());
for (String nodeId : favoriteIds) {
INode node = QueryHelper.nodeById(nodeId);
favoriteNodes.add(node);
}
return favoriteNodes;
}
protected Set<String> parseFavoritesBlobData(String favoritesData) {
Set<String> favoriteIds = new LinkedHashSet<String>();
for (StringTokenizer tokenizer = new StringTokenizer(favoritesData, SEPARATOR); tokenizer.hasMoreTokens();) {
String nodeId = tokenizer.nextToken();
favoriteIds.add(nodeId);
}
return favoriteIds;
}
public void setFavorites(Collection<? extends INode> nodes, IProgressMonitor monitor)
throws NoServiceException, ConflictException, NotAuthorizedException, IllegalStateException, IOException
{
SubMonitor progress = SubMonitor.convert(monitor, Messages.UserFavoritesService_SettingUserFavorites, 1000);
String favoritesData = createFavoritesBlobData(nodes);
try {
if (favoritesData == null || "".equals(favoritesData)) { //$NON-NLS-1$
getFavoritesBlob().delete();
} else {
getFavoritesBlob().setContentsUTF(favoritesData);
}
progress.worked(900);//FIXME waiting for USS bug 488335 to have proper progress and cancelation
} catch (OperationCanceledException ex) {
throw processProtocolException(ex);
} catch (ProtocolException ex) {
throw processProtocolException(ex);
}
synchronized (this) {
SubMonitor notifyNewProgress = SubMonitor.convert(progress.newChild(50), nodes.size());
Set<String> newFavorites = new HashSet<String>();
for (INode node : nodes) {
String id = node.getId();
if (newFavorites.add(id)) {
boolean newFavorite = favorites.add(id);
if (newFavorite) {
didChangeFavorite(id, true);
}
}
notifyNewProgress.worked(1);
}
SubMonitor notifyRemovedProgress = SubMonitor.convert(progress.newChild(50), favorites.size());
for (Iterator<String> i = favorites.iterator(); i.hasNext();) {
String id = i.next();
if (!newFavorites.contains(id)) {
i.remove();
didChangeFavorite(id, false);
}
notifyRemovedProgress.worked(1);
}
}
}
private void didChangeFavorite(String nodeId, boolean favorite) {
Integer correction = favoritesCorrections.get(nodeId);
if (correction == null) {
correction = 0;
}
correction += favorite ? 1 : -1;
if (correction < -1) {
correction = -1;
} else if (correction > 1) {
correction = 1;
}
if (correction == 0) {
favoritesCorrections.remove(nodeId);
} else {
favoritesCorrections.put(nodeId, correction);
}
}
protected String createFavoritesBlobData(Collection<? extends INode> nodes) {
if (nodes.isEmpty()) {
return null;
}
List<String> nodeIds = new ArrayList<String>(nodes.size());
for (INode node : nodes) {
nodeIds.add(node.getId());
}
Collections.sort(nodeIds);
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String nodeId : nodeIds) {
if (first) {
first = false;
} else {
builder.append(SEPARATOR);
}
builder.append(nodeId);
}
return builder.toString();
}
public void setFavorite(INode node, boolean favorite, IProgressMonitor monitor)
throws NotAuthorizedException, ConflictException, IOException {
alterFavorites(Collections.singleton(node), favorite, monitor);
}
public void addFavorites(Collection<? extends INode> nodes, IProgressMonitor monitor)
throws NotAuthorizedException, ConflictException, IOException {
alterFavorites(nodes, true, monitor);
}
public void removeFavorites(Collection<? extends INode> nodes, IProgressMonitor monitor)
throws NotAuthorizedException, ConflictException, IOException {
alterFavorites(nodes, false, monitor);
}
private void alterFavorites(Collection<? extends INode> nodes, boolean favorite, IProgressMonitor monitor)
throws NotAuthorizedException, ConflictException, IOException {
SubMonitor progress = SubMonitor.convert(monitor, Messages.UserFavoritesService_SettingUserFavorites, 1000);
ConflictException conflictException = null;
for (int i = 0; i < RETRY_COUNT; i++) {
try {
progress.setWorkRemaining(1000);
doAlterFavorites(nodes, favorite, progress.newChild(800));
progress.done();
return;
} catch (ConflictException e) {
conflictException = e;
} catch (OperationCanceledException ex) {
throw processProtocolException(ex);
} catch (ProtocolException ex) {
throw processProtocolException(ex);
}
}
if (conflictException != null) {
throw conflictException;
}
}
private void doAlterFavorites(Collection<? extends INode> nodes, boolean favorite, IProgressMonitor monitor)
throws ConflictException, IOException {
SubMonitor progress = SubMonitor.convert(monitor, Messages.UserFavoritesService_SettingUserFavorites, 1000);
List<INode> favorites = getFavorites(progress.newChild(300));
for (INode node : nodes) {
INode currentFavorite = QueryHelper.findById(favorites, node);
if (currentFavorite != null && !favorite) {
favorites.remove(currentFavorite);
} else if (currentFavorite == null && favorite) {
favorites.add(node);
}
}
setFavorites(favorites, progress.newChild(700));
}
public List<INode> getFavorites(URI uri, IProgressMonitor monitor) throws IOException {
List<String> nodeIds = getFavoriteIds(uri, monitor);
return toNodes(nodeIds);
}
public static void validateURI(URI uri) {
if ("".equals(uri.toString()) //$NON-NLS-1$
|| ((uri.getHost() == null || "".equals(uri.getHost())) //$NON-NLS-1$
&& (uri.getScheme() != null && uri.getScheme().toLowerCase().startsWith("http"))) //$NON-NLS-1$
|| (uri.getScheme() == null && (uri.getPath() == null || "".equals(uri.getPath())))) { //$NON-NLS-1$
//incomplete uri
throw new IllegalArgumentException(
new URISyntaxException(uri.toString(), Messages.UserFavoritesService_uriMissingHost));
}
}
public List<String> getFavoriteIds(URI uri, IProgressMonitor monitor) throws IOException {
validateURI(uri);
try {
return new RequestTemplate<List<String>>() {
@Override
protected Request configureRequest(Request request, URI uri) {
return super.configureRequest(request, uri).setHeader(HttpHeaders.USER_AGENT, Session.USER_AGENT_ID)
.addHeader(HttpHeaders.CONTENT_TYPE, Session.APPLICATION_JSON) //
.addHeader(HttpHeaders.ACCEPT, Session.APPLICATION_JSON);
}
@Override
protected List<String> handleResponseStream(InputStream content) throws IOException {
List<String> favoriteIds = new ArrayList<String>();
String body = read(content);
body = body.trim();
if (!"".equals(body)) { //$NON-NLS-1$
Matcher matcher = JSON_MPC_FAVORITES_PATTERN.matcher(body);
if (matcher.find()) {
String favorites = matcher.group(1);
Matcher contentIdMatcher = JSON_CONTENT_ID_PATTERN.matcher(favorites);
while (contentIdMatcher.find()) {
String id = contentIdMatcher.group(1);
favoriteIds.add(id);
}
} else {
throw malformedContentException(uri, body);
}
}
return favoriteIds;
}
@Override
protected Request createRequest(URI uri) {
return Request.Get(uri);
}
}.execute(uri);
} catch (FileNotFoundException e) {
return new ArrayList<String>();
}
}
private static String read(InputStream in) throws IOException {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtil.copy(in, baos);
return StringUtil.fromUTF(baos.toByteArray());
} catch (RuntimeException ex) {
Throwable cause = ex.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
}
throw ex;
} finally {
IOUtil.close(in);
}
}
private static ProtocolException malformedContentException(final URI endpoint, String body) {
return new ProtocolException("GET", endpoint, "1.1", MALFORMED_CONTENT_ERROR_CODE, //$NON-NLS-1$ //$NON-NLS-2$
"Malformed response content: " + body); //$NON-NLS-1$
}
public static boolean isInvalidFavoritesListException(Throwable error) {
while (error != null) {
if (isMalformedContentException(error) || isNotFoundException(error)) {
return true;
}
if (error instanceof CoreException) {
CoreException coreException = (CoreException) error;
IStatus status = coreException.getStatus();
if (status.isMultiStatus()) {
for (IStatus childStatus : status.getChildren()) {
if (childStatus.getException() != null
&& isInvalidFavoritesListException(childStatus.getException())) {
return true;
}
}
}
}
error = error.getCause();
}
return false;
}
public static boolean isInvalidUrlException(Throwable error) {
while (error != null) {
if (error instanceof URISyntaxException || error instanceof MalformedURLException) {
return true;
}
if (error instanceof CoreException) {
CoreException coreException = (CoreException) error;
IStatus status = coreException.getStatus();
if (status.isMultiStatus()) {
for (IStatus childStatus : status.getChildren()) {
if (childStatus.getException() != null && isInvalidUrlException(childStatus.getException())) {
return true;
}
}
}
}
error = error.getCause();
}
return false;
}
private static boolean isMalformedContentException(Throwable error) {
if (error instanceof ProtocolException) {
ProtocolException protocolException = (ProtocolException) error;
switch (protocolException.getStatusCode()) {
case HttpStatus.SC_BAD_REQUEST:
case HttpStatus.SC_METHOD_NOT_ALLOWED:
case HttpStatus.SC_NOT_ACCEPTABLE:
case HttpStatus.SC_EXPECTATION_FAILED:
case MALFORMED_CONTENT_ERROR_CODE:
return true;
default:
return false;
}
}
return false;
}
private static boolean isNotFoundException(Throwable error) {
if (error instanceof NotFoundException) {
return true;
}
if (error instanceof ProtocolException) {
ProtocolException protocolException = (ProtocolException) error;
switch (protocolException.getStatusCode()) {
case HttpStatus.SC_NOT_FOUND:
case HttpStatus.SC_GONE:
return true;
default:
return false;
}
}
return false;
}
}