/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.shindig.gadgets.servlet;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.protocol.BaseRequestItem;
import org.apache.shindig.protocol.Operation;
import org.apache.shindig.protocol.ProtocolException;
import org.apache.shindig.protocol.RequestItem;
import org.apache.shindig.protocol.Service;
import org.apache.shindig.protocol.conversion.BeanDelegator;
import org.apache.shindig.protocol.conversion.BeanFilter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import javax.servlet.http.HttpServletResponse;
/**
* Provides endpoints for gadget metadata lookup and more.
*
* @since 2.0.0
*/
@Service(name = "gadgets")
public class GadgetsHandler {
@VisibleForTesting
static final String FAILURE_METADATA = "Failed to get gadget metadata.";
@VisibleForTesting
static final String FAILURE_TOKEN = "Failed to get gadget token.";
private static final List<String> DEFAULT_METADATA_FIELDS =
ImmutableList.of("iframeUrl", "userPrefs.*", "modulePrefs.*", "views.*", "token");
private static final List<String> DEFAULT_TOKEN_FIELDS = ImmutableList.of("token");
protected final ExecutorService executor;
protected final GadgetsHandlerService handlerService;
protected final BeanFilter beanFilter;
protected final BeanDelegator beanDelegator;
@Inject
public GadgetsHandler(ExecutorService executor, GadgetsHandlerService handlerService,
BeanFilter beanFilter) {
this.executor = executor;
this.handlerService = handlerService;
this.beanFilter = beanFilter;
this.beanDelegator = new BeanDelegator();
}
@Operation(httpMethods = {"POST", "GET"}, path = "metadata.get")
public Map<String, GadgetsHandlerApi.BaseResponse> metadata(BaseRequestItem request)
throws ProtocolException {
return new AbstractExecutor<GadgetsHandlerApi.MetadataResponse>() {
@Override
protected Callable<GadgetsHandlerApi.MetadataResponse> createJob(String url,
BaseRequestItem request) {
return createMetadataJob(url, request);
}
}.execute(request);
}
@Operation(httpMethods = {"POST", "GET"}, path = "token.get")
public Map<String, GadgetsHandlerApi.BaseResponse> token(BaseRequestItem request)
throws ProtocolException {
return new AbstractExecutor<GadgetsHandlerApi.TokenResponse>() {
@Override
protected Callable<GadgetsHandlerApi.TokenResponse> createJob(String url,
BaseRequestItem request) {
return createTokenJob(url, request);
}
}.execute(request);
}
@Operation(httpMethods = "GET", path = "/@metadata.supportedFields")
public Set<String> supportedFields(RequestItem request) {
return ImmutableSet.copyOf(beanFilter
.getBeanFields(GadgetsHandlerApi.MetadataResponse.class, 5));
}
@Operation(httpMethods = "GET", path = "/@token.supportedFields")
public Set<String> tokenSupportedFields(RequestItem request) {
return ImmutableSet.copyOf(
beanFilter.getBeanFields(GadgetsHandlerApi.TokenResponse.class, 5));
}
private abstract class AbstractExecutor<R extends GadgetsHandlerApi.BaseResponse> {
@SuppressWarnings("unchecked")
public Map<String, GadgetsHandlerApi.BaseResponse> execute(BaseRequestItem request) {
Set<String> gadgetUrls = ImmutableSet.copyOf(request.getListParameter("ids"));
if (gadgetUrls.isEmpty()) {
return ImmutableMap.of();
}
CompletionService<R> completionService = new ExecutorCompletionService<R>(executor);
for (String gadgetUrl : gadgetUrls) {
Callable<R> job = createJob(gadgetUrl, request);
completionService.submit(job);
}
ImmutableMap.Builder<String, GadgetsHandlerApi.BaseResponse> builder = ImmutableMap.builder();
for (int numJobs = gadgetUrls.size(); numJobs > 0; numJobs--) {
R response;
try {
response = completionService.take().get();
builder.put(response.getUrl().toString(), response);
} catch (InterruptedException e) {
throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Processing interrupted.", e);
} catch (ExecutionException e) {
if (!(e.getCause() instanceof RpcException)) {
throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Processing error.", e);
}
RpcException cause = (RpcException) e.getCause();
GadgetContext context = cause.getContext();
if (context != null) {
Uri url = context.getUrl();
GadgetsHandlerApi.BaseResponse errorResponse =
handlerService.createBaseResponse(url, cause.getMessage());
builder.put(url.toString(), errorResponse);
}
}
}
return builder.build();
}
protected abstract Callable<R> createJob(String url, BaseRequestItem request);
}
// Hook to override in sub-class.
protected Callable<GadgetsHandlerApi.MetadataResponse> createMetadataJob(String url,
BaseRequestItem request) {
final MetadataRequestData metadataRequest = new MetadataRequestData(url, request);
return new Callable<GadgetsHandlerApi.MetadataResponse>() {
public GadgetsHandlerApi.MetadataResponse call() throws Exception {
try {
return handlerService.getMetadata(metadataRequest);
} catch (Exception e) {
sendError(metadataRequest.getUrl(), e, FAILURE_METADATA);
return null;
}
}
};
}
// Hook to override in sub-class.
protected Callable<GadgetsHandlerApi.TokenResponse> createTokenJob(String url,
BaseRequestItem request) {
final TokenRequestData tokenRequest = new TokenRequestData(url, request);
return new Callable<GadgetsHandlerApi.TokenResponse>() {
public GadgetsHandlerApi.TokenResponse call() throws Exception {
try {
return handlerService.getToken(tokenRequest);
} catch (Exception e) {
sendError(tokenRequest.getUrl(), e, FAILURE_TOKEN);
return null;
}
}
};
}
private void sendError(final Uri url, Exception e, String msg)
throws RpcException {
GadgetContext context = new GadgetContext() {
@Override
public Uri getUrl() { return url; }
};
// Note: this error message is publicly visible in JSON-RPC response.
throw new RpcException(context, msg, e);
}
/**
* Gadget context classes used to translate JSON BaseRequestItem into a more
* meaningful model objects that Java can work with.
*/
private abstract class AbstractRequest implements GadgetsHandlerApi.BaseRequest {
protected final Uri uri;
protected final String container;
protected final List<String> fields;
protected final BaseRequestItem request;
public AbstractRequest(String url, BaseRequestItem request, List<String> defaultFields) {
this.uri = Uri.parse(Preconditions.checkNotNull(url));
this.request = Preconditions.checkNotNull(request);
this.container = Preconditions.checkNotNull(request.getParameter("container"));
this.fields = processFields(request, defaultFields);
}
public Uri getUrl() {
return uri;
}
public String getContainer() {
return container;
}
public List<String> getFields() {
return fields;
}
private List<String> processFields(BaseRequestItem request, List<String> defaultList) {
List<String> value = request.getListParameter(BaseRequestItem.FIELDS);
return ((value == null || value.size() == 0) ? defaultList : value);
}
}
protected class TokenRequestData extends AbstractRequest
implements GadgetsHandlerApi.TokenRequest {
public TokenRequestData(String url, BaseRequestItem request) {
super(url, request, DEFAULT_TOKEN_FIELDS);
}
public GadgetsHandlerApi.TokenData getToken() {
return beanDelegator.createDelegator(
request.getToken(), GadgetsHandlerApi.TokenData.class);
}
}
protected class MetadataRequestData extends AbstractRequest
implements GadgetsHandlerApi.MetadataRequest {
protected final Locale locale;
protected final boolean ignoreCache;
protected final boolean debug;
public MetadataRequestData(String url, BaseRequestItem request) {
super(url, request, DEFAULT_METADATA_FIELDS);
String lang = request.getParameter("language");
String country = request.getParameter("country");
this.locale =
(lang != null && country != null) ? new Locale(lang, country) : (lang != null)
? new Locale(lang) : GadgetSpec.DEFAULT_LOCALE;
this.ignoreCache = Boolean.valueOf(request.getParameter("ignoreCache"));
this.debug = Boolean.valueOf(request.getParameter("debug"));
}
public int getModuleId() {
return 1; // TODO calculate?
}
public Locale getLocale() {
return locale;
}
public boolean getIgnoreCache() {
return ignoreCache;
}
public boolean getDebug() {
return debug;
}
public String getView() {
return request.getParameter("view", "default");
}
public GadgetsHandlerApi.TokenData getToken() {
return beanDelegator.createDelegator(
request.getToken(), GadgetsHandlerApi.TokenData.class);
}
}
}