http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java new file mode 100644 index 0000000..c65c78f --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java @@ -0,0 +1,163 @@ +/* + * 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.brooklyn.rest.filter; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ContextResolver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState; +import org.apache.brooklyn.rest.domain.ApiError; +import org.apache.brooklyn.util.text.Strings; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import com.sun.jersey.api.model.AbstractMethod; +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; +import com.sun.jersey.spi.container.ContainerResponseFilter; +import com.sun.jersey.spi.container.ResourceFilter; +import com.sun.jersey.spi.container.ResourceFilterFactory; + +/** + * Checks that if the method or resource class corresponding to a request + * has a {@link HaHotStateRequired} annotation, + * that the server is in that state (and up). + * Requests with {@link #SKIP_CHECK_HEADER} set as a header skip this check. + * <p> + * This follows a different pattern to {@link HaMasterCheckFilter} + * as this needs to know the method being invoked. + */ +public class HaHotCheckResourceFilter implements ResourceFilterFactory { + public static final String SKIP_CHECK_HEADER = "Brooklyn-Allow-Non-Master-Access"; + + private static final Logger log = LoggerFactory.getLogger(HaHotCheckResourceFilter.class); + + private static final Set<ManagementNodeState> HOT_STATES = ImmutableSet.of( + ManagementNodeState.MASTER, ManagementNodeState.HOT_STANDBY, ManagementNodeState.HOT_BACKUP); + + @Context + private ContextResolver<ManagementContext> mgmt; + + public HaHotCheckResourceFilter() {} + + @VisibleForTesting + public HaHotCheckResourceFilter(ContextResolver<ManagementContext> mgmt) { + this.mgmt = mgmt; + } + + private ManagementContext mgmt() { + return mgmt.getContext(ManagementContext.class); + } + + private static class MethodFilter implements ResourceFilter, ContainerRequestFilter { + + private AbstractMethod am; + private ManagementContext mgmt; + + public MethodFilter(AbstractMethod am, ManagementContext mgmt) { + this.am = am; + this.mgmt = mgmt; + } + + @Override + public ContainerRequestFilter getRequestFilter() { + return this; + } + + @Override + public ContainerResponseFilter getResponseFilter() { + return null; + } + + private String lookForProblem(ContainerRequest request) { + if (isSkipCheckHeaderSet(request)) + return null; + + if (!isHaHotStateRequired(request)) + return null; + + String problem = lookForProblemIfServerNotRunning(mgmt); + if (Strings.isNonBlank(problem)) + return problem; + + if (!isHaHotStatus()) + return "server not in required HA hot state"; + if (isStateNotYetValid()) + return "server not yet completed loading data for required HA hot state"; + + return null; + } + + @Override + public ContainerRequest filter(ContainerRequest request) { + String problem = lookForProblem(request); + if (Strings.isNonBlank(problem)) { + log.warn("Disallowing web request as "+problem+": "+request+"/"+am+" (caller should set '"+SKIP_CHECK_HEADER+"' to force)"); + throw new WebApplicationException(ApiError.builder() + .message("This request is only permitted against an active hot Brooklyn server") + .errorCode(Response.Status.FORBIDDEN).build().asJsonResponse()); + } + return request; + } + + // Maybe there should be a separate state to indicate that we have switched state + // but still haven't finished rebinding. (Previously there was a time delay and an + // isRebinding check, but introducing RebindManager#isAwaitingInitialRebind() seems cleaner.) + private boolean isStateNotYetValid() { + return mgmt.getRebindManager().isAwaitingInitialRebind(); + } + + private boolean isHaHotStateRequired(ContainerRequest request) { + return (am.getAnnotation(HaHotStateRequired.class) != null || + am.getResource().getAnnotation(HaHotStateRequired.class) != null); + } + + private boolean isSkipCheckHeaderSet(ContainerRequest request) { + return "true".equalsIgnoreCase(request.getHeaderValue(SKIP_CHECK_HEADER)); + } + + private boolean isHaHotStatus() { + ManagementNodeState state = mgmt.getHighAvailabilityManager().getNodeState(); + return HOT_STATES.contains(state); + } + + } + + public static String lookForProblemIfServerNotRunning(ManagementContext mgmt) { + if (mgmt==null) return "no management context available"; + if (!mgmt.isRunning()) return "server no longer running"; + if (!mgmt.isStartupComplete()) return "server not in required startup-completed state"; + return null; + } + + @Override + public List<ResourceFilter> create(AbstractMethod am) { + return Collections.<ResourceFilter>singletonList(new MethodFilter(am, mgmt())); + } + +}
http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java new file mode 100644 index 0000000..8a3c1c6 --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java @@ -0,0 +1,40 @@ +/* + * 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.brooklyn.rest.filter; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; + +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerResponse; +import com.sun.jersey.spi.container.ContainerResponseFilter; + +public class NoCacheFilter implements ContainerResponseFilter { + + @Override + public ContainerResponse filter(ContainerRequest request, ContainerResponse response) { + //https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching_FAQ + MultivaluedMap<String, Object> headers = response.getHttpHeaders(); + headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache, no-store"); + headers.putSingle("Pragma", "no-cache"); + headers.putSingle(HttpHeaders.EXPIRES, "0"); + return response; + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java new file mode 100644 index 0000000..d9013f0 --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java @@ -0,0 +1,79 @@ +/* + * 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.brooklyn.rest.filter; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.ws.rs.core.UriBuilder; + +import org.apache.brooklyn.rest.apidoc.RestApiResourceScanner; + +import io.swagger.config.ScannerFactory; +import io.swagger.models.Info; +import io.swagger.models.License; +import io.swagger.models.Swagger; + +/** + * Bootstraps swagger. + * <p> + * Swagger was intended to run as a servlet. + * + * @author Ciprian Ciubotariu <cheepe...@gmx.net> + */ +public class SwaggerFilter implements Filter { + + static Info info = new Info() + .title("Brooklyn API Documentation") + .version("v1") // API version, not BROOKLYN_VERSION + .license(new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html")); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { +// ReflectiveJaxrsScanner scanner = new ReflectiveJaxrsScanner(); +// scanner.setResourcePackage("org.apache.brooklyn.rest.api,org.apache.brooklyn.rest.apidoc,org.apache.brooklyn.rest.resources"); +// ScannerFactory.setScanner(scanner); + ScannerFactory.setScanner(new RestApiResourceScanner()); + + ServletContext context = filterConfig.getServletContext(); + Swagger swagger = new Swagger() + .info(info); + context.setAttribute("swagger", swagger); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApiListingResource.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApiListingResource.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApiListingResource.java new file mode 100644 index 0000000..74f8426 --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApiListingResource.java @@ -0,0 +1,260 @@ +/* + * Copyright 2015 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.brooklyn.rest.resources; + +import com.sun.jersey.spi.container.servlet.WebConfig; +import io.swagger.annotations.ApiOperation; +import io.swagger.config.FilterFactory; +import io.swagger.config.Scanner; +import io.swagger.config.ScannerFactory; +import io.swagger.config.SwaggerConfig; +import io.swagger.core.filter.SpecFilter; +import io.swagger.core.filter.SwaggerSpecFilter; +import io.swagger.jaxrs.Reader; +import io.swagger.jaxrs.config.JaxrsScanner; +import io.swagger.jaxrs.config.ReaderConfigUtils; +import io.swagger.jaxrs.listing.SwaggerSerializers; +import io.swagger.models.Swagger; +import io.swagger.util.Yaml; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ApiListingResource usable within a jersey servlet filter. + * + * Taken from io.swagger:swagger-jaxrs, class + * io.swagger.jaxrs.listing.ApiListingResource, which can only be used within a + * servlet context. We are here using a filter, but jersey has a WebConfig class + * that can substitute ServletConfig and FilterConfig. + * + * @todo Remove when the rest-server is no longer running within a filter (e.g. + * as a standalone OSGi http service) + * + * @author Ciprian Ciubotariu <cheepe...@gmx.net> + */ +public class ApiListingResource { + + static Logger LOGGER = LoggerFactory.getLogger(ApiListingResource.class); + + @Context + ServletContext context; + + boolean initialized = false; + + private static class ServletConfigAdapter implements ServletConfig { + + private final WebConfig webConfig; + + private ServletConfigAdapter(WebConfig webConfig) { + this.webConfig = webConfig; + } + + @Override + public String getServletName() { + return webConfig.getName(); + } + + @Override + public ServletContext getServletContext() { + return webConfig.getServletContext(); + } + + @Override + public String getInitParameter(String name) { + return webConfig.getInitParameter(name); + } + + @Override + public Enumeration<String> getInitParameterNames() { + return webConfig.getInitParameterNames(); + } + + } + + protected synchronized Swagger scan(Application app, WebConfig sc) { + Swagger swagger = null; + Scanner scanner = ScannerFactory.getScanner(); + LOGGER.debug("using scanner " + scanner); + + if (scanner != null) { + SwaggerSerializers.setPrettyPrint(scanner.getPrettyPrint()); + swagger = (Swagger) context.getAttribute("swagger"); + + Set<Class<?>> classes; + if (scanner instanceof JaxrsScanner) { + JaxrsScanner jaxrsScanner = (JaxrsScanner) scanner; + classes = jaxrsScanner.classesFromContext(app, new ServletConfigAdapter(sc)); + } else { + classes = scanner.classes(); + } + if (classes != null) { + Reader reader = new Reader(swagger, ReaderConfigUtils.getReaderConfig(context)); + swagger = reader.read(classes); + if (scanner instanceof SwaggerConfig) { + swagger = ((SwaggerConfig) scanner).configure(swagger); + } else { + SwaggerConfig configurator = (SwaggerConfig) context.getAttribute("reader"); + if (configurator != null) { + LOGGER.debug("configuring swagger with " + configurator); + configurator.configure(swagger); + } else { + LOGGER.debug("no configurator"); + } + } + context.setAttribute("swagger", swagger); + } + } + initialized = true; + return swagger; + } + + private Swagger process( + Application app, + WebConfig sc, + HttpHeaders headers, + UriInfo uriInfo) { + Swagger swagger = (Swagger) context.getAttribute("swagger"); + if (!initialized) { + swagger = scan(app, sc); + } + if (swagger != null) { + SwaggerSpecFilter filterImpl = FilterFactory.getFilter(); + if (filterImpl != null) { + SpecFilter f = new SpecFilter(); + swagger = f.filter(swagger, filterImpl, getQueryParams(uriInfo.getQueryParameters()), getCookies(headers), + getHeaders(headers)); + } + } + return swagger; + } + + @GET + @Produces({MediaType.APPLICATION_JSON, "application/yaml"}) + @ApiOperation(value = "The swagger definition in either JSON or YAML", hidden = true) + @Path("/swagger.{type:json|yaml}") + public Response getListing( + @Context Application app, + @Context WebConfig sc, + @Context HttpHeaders headers, + @Context UriInfo uriInfo, + @PathParam("type") String type) { + if (Strings.isNonBlank(type) && type.trim().equalsIgnoreCase("yaml")) { + return getListingYaml(app, sc, headers, uriInfo); + } else { + return getListingJson(app, sc, headers, uriInfo); + } + } + + @GET + @Produces({MediaType.APPLICATION_JSON}) + @Path("/swagger") + @ApiOperation(value = "The swagger definition in JSON", hidden = true) + public Response getListingJson( + @Context Application app, + @Context WebConfig sc, + @Context HttpHeaders headers, + @Context UriInfo uriInfo) { + Swagger swagger = process(app, sc, headers, uriInfo); + + if (swagger != null) { + return Response.ok().entity(swagger).build(); + } else { + return Response.status(404).build(); + } + } + + @GET + @Produces("application/yaml") + @Path("/swagger") + @ApiOperation(value = "The swagger definition in YAML", hidden = true) + public Response getListingYaml( + @Context Application app, + @Context WebConfig sc, + @Context HttpHeaders headers, + @Context UriInfo uriInfo) { + Swagger swagger = process(app, sc, headers, uriInfo); + try { + if (swagger != null) { + String yaml = Yaml.mapper().writeValueAsString(swagger); + StringBuilder b = new StringBuilder(); + String[] parts = yaml.split("\n"); + for (String part : parts) { + b.append(part); + b.append("\n"); + } + return Response.ok().entity(b.toString()).type("application/yaml").build(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return Response.status(404).build(); + } + + protected Map<String, List<String>> getQueryParams(MultivaluedMap<String, String> params) { + Map<String, List<String>> output = new HashMap<>(); + if (params != null) { + for (String key : params.keySet()) { + List<String> values = params.get(key); + output.put(key, values); + } + } + return output; + } + + protected Map<String, String> getCookies(HttpHeaders headers) { + Map<String, String> output = new HashMap<>(); + if (headers != null) { + for (String key : headers.getCookies().keySet()) { + Cookie cookie = headers.getCookies().get(key); + output.put(key, cookie.getValue()); + } + } + return output; + } + + protected Map<String, List<String>> getHeaders(HttpHeaders headers) { + Map<String, List<String>> output = new HashMap<>(); + if (headers != null) { + for (String key : headers.getRequestHeaders().keySet()) { + List<String> values = headers.getRequestHeaders().get(key); + output.put(key, values); + } + } + return output; + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java new file mode 100644 index 0000000..1cf6523 --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java @@ -0,0 +1,33 @@ +/* + * 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.brooklyn.rest.resources; + + +import javax.ws.rs.Path; + +import io.swagger.annotations.Api; + +/** + * @author Ciprian Ciubotariu <cheepe...@gmx.net> + */ +@Api("API Documentation") +@Path("/apidoc") +public class ApidocResource extends ApiListingResource { + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java new file mode 100644 index 0000000..2b5c19b --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java @@ -0,0 +1,81 @@ +/* + * 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.brooklyn.rest.util; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.sun.jersey.core.impl.provider.entity.FormMultivaluedMapProvider; +import com.sun.jersey.core.util.MultivaluedMapImpl; + +/** + * A MessageBodyReader producing a <code>Map<String, Object></code>, where Object + * is either a <code>String</code>, a <code>List<String></code> or null. + */ +@Provider +@Consumes(MediaType.APPLICATION_FORM_URLENCODED) +public class FormMapProvider implements MessageBodyReader<Map<String, Object>> { + + @Override + public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + if (!Map.class.equals(type) || !(genericType instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterized = (ParameterizedType) genericType; + return parameterized.getActualTypeArguments().length == 2 && + parameterized.getActualTypeArguments()[0] == String.class && + parameterized.getActualTypeArguments()[1] == Object.class; + } + + @Override + public Map<String, Object> readFrom(Class<Map<String, Object>> type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + FormMultivaluedMapProvider delegate = new FormMultivaluedMapProvider(); + MultivaluedMap<String, String> multi = new MultivaluedMapImpl(); + multi = delegate.readFrom(multi, mediaType, entityStream); + + Map<String, Object> map = Maps.newHashMapWithExpectedSize(multi.keySet().size()); + for (String key : multi.keySet()) { + List<String> value = multi.get(key); + if (value.size() > 1) { + map.put(key, Lists.newArrayList(value)); + } else if (value.size() == 1) { + map.put(key, Iterables.getOnlyElement(value)); + } else { + map.put(key, null); + } + } + return map; + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/resources/build-metadata.properties ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/resources/build-metadata.properties b/rest/rest-server-jersey/src/main/resources/build-metadata.properties new file mode 100644 index 0000000..eab85ef --- /dev/null +++ b/rest/rest-server-jersey/src/main/resources/build-metadata.properties @@ -0,0 +1,18 @@ +# 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. +git-sha-1 = ${buildNumber} +git-branch-name = ${scmBranch} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/main/webapp/WEB-INF/web.xml ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/main/webapp/WEB-INF/web.xml b/rest/rest-server-jersey/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..a7f99f4 --- /dev/null +++ b/rest/rest-server-jersey/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,144 @@ +<!DOCTYPE web-app PUBLIC + "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" + "http://java.sun.com/dtd/web-app_2_3.dtd" > +<!-- + 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. +--> +<web-app> + <display-name>Brooklyn REST API v1</display-name> + + <filter> + <filter-name>Brooklyn Request Tagging Filter</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.RequestTaggingFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn Request Tagging Filter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <filter> + <filter-name>Brooklyn Properties Authentication Filter</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn Properties Authentication Filter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <filter> + <filter-name>Brooklyn Logging Filter</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.LoggingFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn Logging Filter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <filter> + <filter-name>Brooklyn HA Master Filter</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.HaMasterCheckFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn HA Master Filter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <filter> + <filter-name>Brooklyn Swagger Bootstrap</filter-name> + <filter-class>org.apache.brooklyn.rest.filter.SwaggerFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>Brooklyn Swagger Bootstrap</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <!-- Brooklyn REST is usually run as a filter so static content can be placed in a webapp + to which this is added; to run as a servlet directly, replace the filter tags + below (after the comment) with the servlet tags (commented out immediately below), + (and do the same for the matching tags at the bottom) + <servlet> + <servlet-name>Brooklyn REST API v1 Servlet</servlet-name> + <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> + --> + <filter> + <filter-name>Brooklyn REST API v1 Filter</filter-name> + <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class> + + <!-- load our REST API jersey resources explicitly + (the package scanner will only pick up classes with @Path annotations - doesn't look at implemented interfaces) + --> + <init-param> + <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name> + <param-value>com.sun.jersey.api.core.ClassNamesResourceConfig</param-value> + </init-param> + <init-param> + <param-name>com.sun.jersey.config.property.classnames</param-name> + <param-value> + io.swagger.jaxrs.listing.SwaggerSerializers; + org.apache.brooklyn.rest.util.FormMapProvider; + com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; + org.apache.brooklyn.rest.resources.AccessResource; + org.apache.brooklyn.rest.resources.ActivityResource; + org.apache.brooklyn.rest.resources.ApidocResource; + org.apache.brooklyn.rest.resources.ApplicationResource; + org.apache.brooklyn.rest.resources.CatalogResource; + org.apache.brooklyn.rest.resources.EffectorResource; + org.apache.brooklyn.rest.resources.EntityConfigResource; + org.apache.brooklyn.rest.resources.EntityResource; + org.apache.brooklyn.rest.resources.LocationResource; + org.apache.brooklyn.rest.resources.PolicyConfigResource; + org.apache.brooklyn.rest.resources.PolicyResource; + org.apache.brooklyn.rest.resources.ScriptResource; + org.apache.brooklyn.rest.resources.SensorResource; + org.apache.brooklyn.rest.resources.ServerResource; + org.apache.brooklyn.rest.resources.UsageResource; + org.apache.brooklyn.rest.resources.VersionResource; + </param-value> + </init-param> + + <init-param> + <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> + <param-value>true</param-value> + </init-param> + + <!-- no need for WADL. of course you can turn it back on it you want. --> + <init-param> + <param-name>com.sun.jersey.config.feature.DisableWADL</param-name> + <param-value>true</param-value> + </init-param> + + <init-param> + <param-name>com.sun.jersey.config.feature.FilterContextPath</param-name> + <param-value>/v1</param-value> + </init-param> + + </filter> + <filter-mapping> + <filter-name>Brooklyn REST API v1 Filter</filter-name> + <url-pattern>/v1/*</url-pattern> + </filter-mapping> + <!-- Brooklyn REST as a filter above; replace above 5 lines with those commented out below, + to run it as a servlet (see note above) + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>Brooklyn REST API v1 Servlet</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + --> +</web-app> http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java new file mode 100644 index 0000000..e855841 --- /dev/null +++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java @@ -0,0 +1,151 @@ +/* + * 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.brooklyn.rest; + +import static org.testng.Assert.assertTrue; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.http.HttpHeaders; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.entity.ContentType; +import org.apache.http.message.BasicNameValuePair; +import org.eclipse.jetty.server.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; +import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.http.HttpTool; +import org.apache.brooklyn.util.http.HttpToolResponse; +import org.apache.brooklyn.util.time.Time; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +public class BrooklynPropertiesSecurityFilterTest extends BrooklynRestApiLauncherTestFixture { + + private static final Logger LOG = LoggerFactory.getLogger(BrooklynPropertiesSecurityFilterTest.class); + + /* + Exception java.lang.AssertionError + + Message: error creating app. response code=400 expected [true] but found [false] + Stacktrace: + + + at org.testng.Assert.fail(Assert.java:94) + at org.testng.Assert.failNotEquals(Assert.java:494) + at org.testng.Assert.assertTrue(Assert.java:42) + at org.apache.brooklyn.rest.BrooklynPropertiesSecurityFilterTest.startAppAtNode(BrooklynPropertiesSecurityFilterTest.java:94) + at org.apache.brooklyn.rest.BrooklynPropertiesSecurityFilterTest.testInteractionOfSecurityFilterAndFormMapProvider(BrooklynPropertiesSecurityFilterTest.java:64) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:606) + at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:84) + at org.testng.internal.Invoker.invokeMethod(Invoker.java:714) + at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:901) + at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1231) + at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:127) + at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:111) + at org.testng.TestRunner.privateRun(TestRunner.java:767) + at org.testng.TestRunner.run(TestRunner.java:617) + at org.testng.SuiteRunner.runTest(SuiteRunner.java:348) + at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:343) + at org.testng.SuiteRunner.privateRun(SuiteRunner.java:305) + at org.testng.SuiteRunner.run(SuiteRunner.java:254) + at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52) + at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86) + at org.testng.TestNG.runSuitesSequentially(TestNG.java:1224) + at org.testng.TestNG.runSuitesLocally(TestNG.java:1149) + at org.testng.TestNG.run(TestNG.java:1057) + at org.apache.maven.surefire.testng.TestNGExecutor.run(TestNGExecutor.java:115) + at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.executeMulti(TestNGDirectoryTestSuite.java:205) + at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.execute(TestNGDirectoryTestSuite.java:108) + at org.apache.maven.surefire.testng.TestNGProvider.invoke(TestNGProvider.java:111) + at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:203) + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:155) + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:103) + */ + // Would be great for this to be a unit test but it takes almost ten seconds. + @Test(groups = {"Integration","Broken"}) + public void testInteractionOfSecurityFilterAndFormMapProvider() throws Exception { + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + Server server = useServerForTest(BrooklynRestApiLauncher.launcher() + .securityProvider(AnyoneSecurityProvider.class) + .forceUseOfDefaultCatalogWithJavaClassPath(true) + .withoutJsgui() + .start()); + String appId = startAppAtNode(server); + String entityId = getTestEntityInApp(server, appId); + HttpClient client = HttpTool.httpClientBuilder() + .uri(getBaseUri(server)) + .build(); + List<? extends NameValuePair> nvps = Lists.newArrayList( + new BasicNameValuePair("arg", "bar")); + String effector = String.format("/v1/applications/%s/entities/%s/effectors/identityEffector", appId, entityId); + HttpToolResponse response = HttpTool.httpPost(client, URI.create(getBaseUri() + effector), + ImmutableMap.of(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()), + URLEncodedUtils.format(nvps, Charsets.UTF_8).getBytes()); + + LOG.info("Effector response: {}", response.getContentAsString()); + assertTrue(HttpTool.isStatusCodeHealthy(response.getResponseCode()), "response code=" + response.getResponseCode()); + } finally { + LOG.info("testInteractionOfSecurityFilterAndFormMapProvider complete in " + Time.makeTimeStringRounded(stopwatch)); + } + } + + private String startAppAtNode(Server server) throws Exception { + String blueprint = "name: TestApp\n" + + "location: localhost\n" + + "services:\n" + + "- type: org.apache.brooklyn.test.entity.TestEntity"; + HttpClient client = HttpTool.httpClientBuilder() + .uri(getBaseUri(server)) + .build(); + HttpToolResponse response = HttpTool.httpPost(client, URI.create(getBaseUri() + "/v1/applications"), + ImmutableMap.of(HttpHeaders.CONTENT_TYPE, "application/x-yaml"), + blueprint.getBytes()); + assertTrue(HttpTool.isStatusCodeHealthy(response.getResponseCode()), "error creating app. response code=" + response.getResponseCode()); + @SuppressWarnings("unchecked") + Map<String, Object> body = new ObjectMapper().readValue(response.getContent(), HashMap.class); + return (String) body.get("entityId"); + } + + @SuppressWarnings("rawtypes") + private String getTestEntityInApp(Server server, String appId) throws Exception { + HttpClient client = HttpTool.httpClientBuilder() + .uri(getBaseUri(server)) + .build(); + List entities = new ObjectMapper().readValue( + HttpTool.httpGet(client, URI.create(getBaseUri() + "/v1/applications/" + appId + "/entities"), MutableMap.<String, String>of()).getContent(), List.class); + LOG.info((String) ((Map) entities.get(0)).get("id")); + return (String) ((Map) entities.get(0)).get("id"); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java new file mode 100644 index 0000000..b47a591 --- /dev/null +++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java @@ -0,0 +1,499 @@ +/* + * 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.brooklyn.rest; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.EnumSet; +import java.util.List; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherAbstract; +import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherNoServer; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.core.server.BrooklynServerConfig; +import org.apache.brooklyn.core.server.BrooklynServiceAttributes; +import org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter; +import org.apache.brooklyn.rest.filter.HaMasterCheckFilter; +import org.apache.brooklyn.rest.filter.LoggingFilter; +import org.apache.brooklyn.rest.filter.NoCacheFilter; +import org.apache.brooklyn.rest.filter.RequestTaggingFilter; +import org.apache.brooklyn.rest.filter.SwaggerFilter; +import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; +import org.apache.brooklyn.rest.security.provider.SecurityProvider; +import org.apache.brooklyn.rest.util.ManagementContextProvider; +import org.apache.brooklyn.rest.util.NullServletConfigProvider; +import org.apache.brooklyn.rest.util.ServerStoppingShutdownHandler; +import org.apache.brooklyn.rest.util.ShutdownHandlerProvider; +import org.apache.brooklyn.util.core.osgi.Compat; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.net.Networking; +import org.apache.brooklyn.util.text.WildcardGlobs; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.webapp.WebAppContext; +import org.reflections.util.ClasspathHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.io.Files; +import com.sun.jersey.api.core.DefaultResourceConfig; +import com.sun.jersey.api.core.ResourceConfig; +import com.sun.jersey.spi.container.servlet.ServletContainer; + +/** Convenience and demo for launching programmatically. Also used for automated tests. + * <p> + * BrooklynLauncher has a more full-featured CLI way to start, + * but if you want more control you can: + * <li> take the WAR this project builds (REST API) -- NB probably want the unshaded one (containing all deps) + * <li> take the WAR from the brooklyn-jsgui project (brooklyn-ui repo) _and_ this WAR and combine them + * (this one should run as a filter on the others, _not_ as a ResourceCollection where they fight over who's got root) + * <li> programmatically install things, following the examples herein; + * in particular {@link #installAsServletFilter(ServletContextHandler)} is quite handy! + * <p> + * You can also just run this class. In most installs it just works, assuming your IDE or maven-fu gives you the classpath. + * Add more apps and entities on the classpath and they'll show up in the catalog. + **/ +public class BrooklynRestApiLauncher { + + private static final Logger log = LoggerFactory.getLogger(BrooklynRestApiLauncher.class); + final static int FAVOURITE_PORT = 8081; + public static final String SCANNING_CATALOG_BOM_URL = "classpath://brooklyn/scanning.catalog.bom"; + + enum StartMode { + FILTER, SERVLET, /** web-xml is not fully supported */ @Beta WEB_XML + } + + public static final List<Class<? extends Filter>> DEFAULT_FILTERS = ImmutableList.of( + RequestTaggingFilter.class, + BrooklynPropertiesSecurityFilter.class, + LoggingFilter.class, + HaMasterCheckFilter.class, + SwaggerFilter.class); + + private boolean forceUseOfDefaultCatalogWithJavaClassPath = false; + private Class<? extends SecurityProvider> securityProvider; + private List<Class<? extends Filter>> filters = DEFAULT_FILTERS; + private StartMode mode = StartMode.FILTER; + private ManagementContext mgmt; + private ContextHandler customContext; + private boolean deployJsgui = true; + private boolean disableHighAvailability = true; + private ServerStoppingShutdownHandler shutdownListener; + + protected BrooklynRestApiLauncher() {} + + public BrooklynRestApiLauncher managementContext(ManagementContext mgmt) { + this.mgmt = mgmt; + return this; + } + + public BrooklynRestApiLauncher forceUseOfDefaultCatalogWithJavaClassPath(boolean forceUseOfDefaultCatalogWithJavaClassPath) { + this.forceUseOfDefaultCatalogWithJavaClassPath = forceUseOfDefaultCatalogWithJavaClassPath; + return this; + } + + public BrooklynRestApiLauncher securityProvider(Class<? extends SecurityProvider> securityProvider) { + this.securityProvider = securityProvider; + return this; + } + + /** + * Runs the server with the given set of filters. + * Overrides any previously supplied set (or {@link #DEFAULT_FILTERS} which is used by default). + */ + public BrooklynRestApiLauncher filters(@SuppressWarnings("unchecked") Class<? extends Filter>... filters) { + this.filters = Lists.newArrayList(filters); + return this; + } + + public BrooklynRestApiLauncher mode(StartMode mode) { + this.mode = checkNotNull(mode, "mode"); + return this; + } + + /** Overrides start mode to use an explicit context */ + public BrooklynRestApiLauncher customContext(ContextHandler customContext) { + this.customContext = checkNotNull(customContext, "customContext"); + return this; + } + + public BrooklynRestApiLauncher withJsgui() { + this.deployJsgui = true; + return this; + } + + public BrooklynRestApiLauncher withoutJsgui() { + this.deployJsgui = false; + return this; + } + + public BrooklynRestApiLauncher disableHighAvailability(boolean value) { + this.disableHighAvailability = value; + return this; + } + + public Server start() { + if (this.mgmt == null) { + mgmt = new LocalManagementContext(); + } + BrooklynCampPlatformLauncherAbstract platform = new BrooklynCampPlatformLauncherNoServer() + .useManagementContext(mgmt) + .launch(); + ((LocalManagementContext)mgmt).noteStartupComplete(); + log.debug("started "+platform); + + ContextHandler context; + String summary; + if (customContext == null) { + switch (mode) { + case SERVLET: + context = servletContextHandler(mgmt); + summary = "programmatic Jersey ServletContainer servlet"; + break; + case WEB_XML: + context = webXmlContextHandler(mgmt); + summary = "from WAR at " + ((WebAppContext) context).getWar(); + break; + case FILTER: + default: + context = filterContextHandler(mgmt); + summary = "programmatic Jersey ServletContainer filter on webapp at " + ((WebAppContext) context).getWar(); + break; + } + } else { + context = customContext; + summary = (context instanceof WebAppContext) + ? "from WAR at " + ((WebAppContext) context).getWar() + : "from custom context"; + } + + if (securityProvider != null) { + ((BrooklynProperties) mgmt.getConfig()).put( + BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, securityProvider.getName()); + } + + if (forceUseOfDefaultCatalogWithJavaClassPath) { + // sets URLs for a surefire + ((BrooklynProperties) mgmt.getConfig()).put(BrooklynServerConfig.BROOKLYN_CATALOG_URL, SCANNING_CATALOG_BOM_URL); + ((LocalManagementContext) mgmt).setBaseClassPathForScanning(ClasspathHelper.forJavaClassPath()); + } else { + // don't use any catalog.xml which is set + ((BrooklynProperties) mgmt.getConfig()).put(BrooklynServerConfig.BROOKLYN_CATALOG_URL, ManagementContextInternal.EMPTY_CATALOG_URL); + } + + Server server = startServer(mgmt, context, summary, disableHighAvailability); + if (shutdownListener!=null) { + // not available in some modes, eg webapp + shutdownListener.setServer(server); + } + return server; + } + + private ContextHandler filterContextHandler(ManagementContext mgmt) { + WebAppContext context = new WebAppContext(); + context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, mgmt); + context.setContextPath("/"); + installWar(context); + installAsServletFilter(context, this.filters); + return context; + } + + private void installWar(WebAppContext context) { + // here we run with the JS GUI, for convenience, if we can find it, else set up an empty dir + // TODO pretty sure there is an option to monitor this dir and load changes to static content + // NOTE: When running Brooklyn from an IDE (i.e. by launching BrooklynJavascriptGuiLauncher.main()) + // you will need to ensure that the working directory is set to the brooklyn-ui repo folder. For IntelliJ, + // set the 'Working directory' of the Run/Debug Configuration to $MODULE_DIR$/brooklyn-server/launcher. + // For Eclipse, use the default option of ${workspace_loc:brooklyn-launcher}. + // If the working directory is not set correctly, Brooklyn will be unable to find the jsgui .war + // file and the 'gui not available' message will be shown. + context.setWar(this.deployJsgui && findJsguiWebappInSource().isPresent() + ? findJsguiWebappInSource().get() + : createTempWebDirWithIndexHtml("Brooklyn REST API <p> (gui not available)")); + } + + private ContextHandler servletContextHandler(ManagementContext managementContext) { + ResourceConfig config = new DefaultResourceConfig(); + for (Object r: BrooklynRestApi.getAllResources()) + config.getSingletons().add(r); + config.getSingletons().add(new ManagementContextProvider(mgmt)); + addShutdownListener(config, mgmt); + + + WebAppContext context = new WebAppContext(); + context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, managementContext); + ServletHolder servletHolder = new ServletHolder(new ServletContainer(config)); + context.addServlet(servletHolder, "/v1/*"); + context.setContextPath("/"); + + installWar(context); + installBrooklynFilters(context, this.filters); + return context; + } + + /** NB: not fully supported; use one of the other {@link StartMode}s */ + private ContextHandler webXmlContextHandler(ManagementContext mgmt) { + // TODO add security to web.xml + WebAppContext context; + if (findMatchingFile("src/main/webapp")!=null) { + // running in source mode; need to use special classpath + context = new WebAppContext("src/main/webapp", "/"); + context.setExtraClasspath("./target/classes"); + } else if (findRestApiWar()!=null) { + context = new WebAppContext(findRestApiWar(), "/"); + } else { + throw new IllegalStateException("Cannot find WAR for REST API. Expected in target/*.war, Maven repo, or in source directories."); + } + context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, mgmt); + // TODO shutdown hook + + return context; + } + + /** starts a server, on all NICs if security is configured, + * otherwise (no security) only on loopback interface + * @deprecated since 0.9.0 becoming private */ + @Deprecated + public static Server startServer(ManagementContext mgmt, ContextHandler context, String summary, boolean disableHighAvailability) { + // TODO this repeats code in BrooklynLauncher / WebServer. should merge the two paths. + boolean secure = mgmt != null && !BrooklynWebConfig.hasNoSecurityOptions(mgmt.getConfig()); + if (secure) { + log.debug("Detected security configured, launching server on all network interfaces"); + } else { + log.debug("Detected no security configured, launching server on loopback (localhost) network interface only"); + if (mgmt!=null) { + log.debug("Detected no security configured, running on loopback; disabling authentication"); + ((BrooklynProperties)mgmt.getConfig()).put(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, AnyoneSecurityProvider.class.getName()); + } + } + if (mgmt != null && disableHighAvailability) + mgmt.getHighAvailabilityManager().disabled(); + InetSocketAddress bindLocation = new InetSocketAddress( + secure ? Networking.ANY_NIC : Networking.LOOPBACK, + Networking.nextAvailablePort(FAVOURITE_PORT)); + return startServer(context, summary, bindLocation); + } + + /** @deprecated since 0.9.0 becoming private */ + @Deprecated + public static Server startServer(ContextHandler context, String summary, InetSocketAddress bindLocation) { + Server server = new Server(bindLocation); + + server.setHandler(context); + try { + server.start(); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + log.info("Brooklyn REST server started ("+summary+") on"); + log.info(" http://localhost:"+((NetworkConnector)server.getConnectors()[0]).getLocalPort()+"/"); + + return server; + } + + public static BrooklynRestApiLauncher launcher() { + return new BrooklynRestApiLauncher(); + } + + public static void main(String[] args) throws Exception { + startRestResourcesViaFilter(); + log.info("Press Ctrl-C to quit."); + } + + public static Server startRestResourcesViaFilter() { + return new BrooklynRestApiLauncher() + .mode(StartMode.FILTER) + .start(); + } + + public static Server startRestResourcesViaServlet() throws Exception { + return new BrooklynRestApiLauncher() + .mode(StartMode.SERVLET) + .start(); + } + + public static Server startRestResourcesViaWebXml() throws Exception { + return new BrooklynRestApiLauncher() + .mode(StartMode.WEB_XML) + .start(); + } + + public void installAsServletFilter(ServletContextHandler context) { + installAsServletFilter(context, DEFAULT_FILTERS); + } + + private void installAsServletFilter(ServletContextHandler context, List<Class<? extends Filter>> filters) { + installBrooklynFilters(context, filters); + + // now set up the REST servlet resources + ResourceConfig config = new DefaultResourceConfig(); + // load all our REST API modules, JSON, and Swagger + for (Object r: BrooklynRestApi.getAllResources()) + config.getSingletons().add(r); + + // disable caching for dynamic content + config.getProperties().put(ResourceConfig.PROPERTY_CONTAINER_RESPONSE_FILTERS, NoCacheFilter.class.getName()); + // Checks if appropriate request given HA status + config.getProperties().put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter.class.getName()); + // configure to match empty path, or any thing which looks like a file path with /assets/ and extension html, css, js, or png + // and treat that as static content + config.getProperties().put(ServletContainer.PROPERTY_WEB_PAGE_CONTENT_REGEX, "(/?|[^?]*/assets/[^?]+\\.[A-Za-z0-9_]+)"); + // and anything which is not matched as a servlet also falls through (but more expensive than a regex check?) + config.getFeatures().put(ServletContainer.FEATURE_FILTER_FORWARD_ON_404, true); + // finally create this as a _filter_ which falls through to a web app or something (optionally) + FilterHolder filterHolder = new FilterHolder(new ServletContainer(config)); + // Let the filter know the context path where it lives + filterHolder.setInitParameter(ServletContainer.PROPERTY_FILTER_CONTEXT_PATH, "/v1"); + context.addFilter(filterHolder, "/v1/*", EnumSet.allOf(DispatcherType.class)); + + ManagementContext mgmt = getManagementContext(context); + config.getSingletons().add(new ManagementContextProvider(mgmt)); + addShutdownListener(config, mgmt); + } + + protected synchronized void addShutdownListener(ResourceConfig config, ManagementContext mgmt) { + if (shutdownListener!=null) throw new IllegalStateException("Can only retrieve one shutdown listener"); + shutdownListener = new ServerStoppingShutdownHandler(mgmt); + config.getSingletons().add(new ShutdownHandlerProvider(shutdownListener)); + } + + private static void installBrooklynFilters(ServletContextHandler context, List<Class<? extends Filter>> filters) { + for (Class<? extends Filter> filter : filters) { + context.addFilter(filter, "/*", EnumSet.allOf(DispatcherType.class)); + } + } + + /** + * Starts the server on all nics (even if security not enabled). + * @deprecated since 0.6.0; use {@link #launcher()} and set a custom context + */ + @Deprecated + public static Server startServer(ContextHandler context, String summary) { + return BrooklynRestApiLauncher.startServer(context, summary, + new InetSocketAddress(Networking.ANY_NIC, Networking.nextAvailablePort(FAVOURITE_PORT))); + } + + /** look for the JS GUI webapp in common source places, returning path to it if found, or null. + * assumes `brooklyn-ui` is checked out as a sibling to `brooklyn-server`, and both are 2, 3, 1, or 0 + * levels above the CWD. */ + @Beta + public static Maybe<String> findJsguiWebappInSource() { + // normally up 2 levels to where brooklyn-* folders are, then into ui + // (but in rest projects it might be 3 up, and in some IDEs we might run from parent dirs.) + // TODO could also look in maven repo ? + return findFirstMatchingFile( + "../../brooklyn-ui/src/main/webapp", + "../../../brooklyn-ui/src/main/webapp", + "../brooklyn-ui/src/main/webapp", + "./brooklyn-ui/src/main/webapp", + "../../brooklyn-ui/target/*.war", + "../../..brooklyn-ui/target/*.war", + "../brooklyn-ui/target/*.war", + "./brooklyn-ui/target/*.war"); + } + + /** look for the REST WAR file in common places, returning path to it if found, or null */ + private static String findRestApiWar() { + // don't look at src/main/webapp here -- because classes won't be there! + // could also look in maven repo ? + // TODO looks like this stopped working at runtime a long time ago; + // only needed for WEB_XML mode, and not used, but should remove or check? + // (probably will be superseded by CXF/OSGi work however) + return findMatchingFile("../rest/target/*.war").orNull(); + } + + /** as {@link #findMatchingFile(String)} but finding the first */ + public static Maybe<String> findFirstMatchingFile(String ...filenames) { + for (String f: filenames) { + Maybe<String> result = findMatchingFile(f); + if (result.isPresent()) return result; + } + return Maybe.absent(); + } + + /** returns the supplied filename if it exists (absolute or relative to the current directory); + * supports globs in the filename portion only, in which case it returns the _newest_ matching file. + * <p> + * otherwise returns null */ + @Beta // public because used in dependent test projects + public static Maybe<String> findMatchingFile(String filename) { + final File f = new File(filename); + if (f.exists()) return Maybe.of(filename); + File dir = f.getParentFile(); + File result = null; + if (dir.exists()) { + File[] matchingFiles = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return WildcardGlobs.isGlobMatched(f.getName(), name); + } + }); + for (File mf: matchingFiles) { + if (result==null || mf.lastModified() > result.lastModified()) result = mf; + } + } + if (result==null) return Maybe.absent(); + return Maybe.of(result.getAbsolutePath()); + } + + /** create a directory with a simple index.html so we have some content being served up */ + private static String createTempWebDirWithIndexHtml(String indexHtmlContent) { + File dir = Files.createTempDir(); + dir.deleteOnExit(); + try { + Files.write(indexHtmlContent, new File(dir, "index.html"), Charsets.UTF_8); + } catch (IOException e) { + Exceptions.propagate(e); + } + return dir.getAbsolutePath(); + } + + /** + * Compatibility methods between karaf launcher and monolithic launcher. + * + * @todo Remove after transition to karaf launcher. + */ + static ManagementContext getManagementContext(ContextHandler jettyServerHandler) { + ManagementContext managementContext = Compat.getInstance().getManagementContext(); + if (managementContext == null && jettyServerHandler != null) { + managementContext = (ManagementContext) jettyServerHandler.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT); + } + return managementContext; + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java new file mode 100644 index 0000000..1bf756d --- /dev/null +++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java @@ -0,0 +1,77 @@ +/* + * 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.brooklyn.rest; + +import static org.apache.brooklyn.rest.BrooklynRestApiLauncher.StartMode.FILTER; +import static org.apache.brooklyn.rest.BrooklynRestApiLauncher.StartMode.SERVLET; +import static org.apache.brooklyn.rest.BrooklynRestApiLauncher.StartMode.WEB_XML; + +import java.util.concurrent.Callable; + +import org.apache.brooklyn.entity.brooklynnode.BrooklynNode; +import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.http.HttpAsserts; +import org.apache.brooklyn.util.http.HttpTool; +import org.apache.http.HttpStatus; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Server; +import org.testng.annotations.Test; + +public class BrooklynRestApiLauncherTest extends BrooklynRestApiLauncherTestFixture { + + @Test + public void testFilterStart() throws Exception { + checkRestCatalogEntities(useServerForTest(baseLauncher().mode(FILTER).start())); + } + + @Test + public void testServletStart() throws Exception { + checkRestCatalogEntities(useServerForTest(baseLauncher().mode(SERVLET).start())); + } + + @Test + public void testWebAppStart() throws Exception { + checkRestCatalogEntities(useServerForTest(baseLauncher().mode(WEB_XML).start())); + } + + private BrooklynRestApiLauncher baseLauncher() { + return BrooklynRestApiLauncher.launcher() + .securityProvider(AnyoneSecurityProvider.class) + .forceUseOfDefaultCatalogWithJavaClassPath(true); + } + + private static void checkRestCatalogEntities(Server server) throws Exception { + final String rootUrl = "http://localhost:"+((NetworkConnector)server.getConnectors()[0]).getLocalPort(); + int code = Asserts.succeedsEventually(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + int code = HttpTool.getHttpStatusCode(rootUrl+"/v1/catalog/entities"); + if (code == HttpStatus.SC_FORBIDDEN) { + throw new RuntimeException("Retry request"); + } else { + return code; + } + } + }); + HttpAsserts.assertHealthyStatusCode(code); + HttpAsserts.assertContentContainsText(rootUrl+"/v1/catalog/entities", BrooklynNode.class.getSimpleName()); + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTestFixture.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTestFixture.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTestFixture.java new file mode 100644 index 0000000..c894f3e --- /dev/null +++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTestFixture.java @@ -0,0 +1,109 @@ +/* + * 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.brooklyn.rest; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.server.BrooklynServerConfig; +import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.reflections.util.ClasspathHelper; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; + +public abstract class BrooklynRestApiLauncherTestFixture { + + Server server = null; + + @AfterMethod(alwaysRun=true) + public void stopServer() throws Exception { + if (server!=null) { + ManagementContext mgmt = getManagementContextFromJettyServerAttributes(server); + server.stop(); + if (mgmt!=null) Entities.destroyAll(mgmt); + server = null; + } + } + + protected Server newServer() { + try { + Server server = BrooklynRestApiLauncher.launcher() + .forceUseOfDefaultCatalogWithJavaClassPath(true) + .securityProvider(AnyoneSecurityProvider.class) + .start(); + return server; + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + + protected Server useServerForTest(Server server) { + if (this.server!=null) { + Assert.fail("Test only meant for single server; already have "+this.server+" when checking "+server); + } else { + this.server = server; + } + return server; + } + + protected String getBaseUri() { + return getBaseUri(server); + } + public static String getBaseUri(Server server) { + return "http://localhost:"+((NetworkConnector)server.getConnectors()[0]).getLocalPort(); + } + + public static void forceUseOfDefaultCatalogWithJavaClassPath(Server server) { + ManagementContext mgmt = getManagementContextFromJettyServerAttributes(server); + forceUseOfDefaultCatalogWithJavaClassPath(mgmt); + } + + public static void forceUseOfDefaultCatalogWithJavaClassPath(ManagementContext manager) { + // TODO duplication with BrooklynRestApiLauncher ? + + // don't use any catalog.xml which is set + ((BrooklynProperties)manager.getConfig()).put(BrooklynServerConfig.BROOKLYN_CATALOG_URL, BrooklynRestApiLauncher.SCANNING_CATALOG_BOM_URL); + // sets URLs for a surefire + ((LocalManagementContext)manager).setBaseClassPathForScanning(ClasspathHelper.forJavaClassPath()); + // this also works +// ((LocalManagementContext)manager).setBaseClassPathForScanning(ClasspathHelper.forPackage("brooklyn")); + // but this (near-default behaviour) does not +// ((LocalManagementContext)manager).setBaseClassLoader(getClass().getClassLoader()); + } + + public static void enableAnyoneLogin(Server server) { + ManagementContext mgmt = getManagementContextFromJettyServerAttributes(server); + enableAnyoneLogin(mgmt); + } + + public static void enableAnyoneLogin(ManagementContext mgmt) { + ((BrooklynProperties)mgmt.getConfig()).put(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, + AnyoneSecurityProvider.class.getName()); + } + + public static ManagementContext getManagementContextFromJettyServerAttributes(Server server) { + return BrooklynRestApiLauncher.getManagementContext((ContextHandler) server.getHandler()); + } + +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/HaHotCheckTest.java ---------------------------------------------------------------------- diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/HaHotCheckTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/HaHotCheckTest.java new file mode 100644 index 0000000..73dfe5f --- /dev/null +++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/HaHotCheckTest.java @@ -0,0 +1,130 @@ +/* + * 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.brooklyn.rest; + +import static org.testng.Assert.assertEquals; + +import javax.ws.rs.core.MediaType; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityManager; +import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode; +import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter; +import org.apache.brooklyn.rest.filter.HaMasterCheckFilter; +import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; +import org.apache.brooklyn.rest.util.HaHotStateCheckClassResource; +import org.apache.brooklyn.rest.util.HaHotStateCheckResource; +import org.apache.brooklyn.rest.util.ManagementContextProvider; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource.Builder; +import com.sun.jersey.api.core.ResourceConfig; + +public class HaHotCheckTest extends BrooklynRestResourceTest { + + // setup and teardown before/after each method + + @BeforeMethod(alwaysRun = true) + public void setUp() throws Exception { super.setUp(); } + + @AfterMethod(alwaysRun = true) + public void tearDown() throws Exception { super.tearDown(); } + + @Override + protected void addBrooklynResources() { + config.getProperties().put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, + new HaHotCheckResourceFilter(new ManagementContextProvider(getManagementContext()))); + addResource(new HaHotStateCheckResource()); + addResource(new HaHotStateCheckClassResource()); + + ((LocalManagementContext)getManagementContext()).noteStartupComplete(); + } + + @Test + public void testHaCheck() { + HighAvailabilityManager ha = getManagementContext().getHighAvailabilityManager(); + assertEquals(ha.getNodeState(), ManagementNodeState.MASTER); + testResourceFetch("/v1/ha/method/ok", 200); + testResourceFetch("/v1/ha/method/fail", 200); + testResourceFetch("/v1/ha/class/fail", 200); + + getManagementContext().getHighAvailabilityManager().changeMode(HighAvailabilityMode.STANDBY); + assertEquals(ha.getNodeState(), ManagementNodeState.STANDBY); + + testResourceFetch("/v1/ha/method/ok", 200); + testResourceFetch("/v1/ha/method/fail", 403); + testResourceFetch("/v1/ha/class/fail", 403); + + ((ManagementContextInternal)getManagementContext()).terminate(); + assertEquals(ha.getNodeState(), ManagementNodeState.TERMINATED); + + testResourceFetch("/v1/ha/method/ok", 200); + testResourceFetch("/v1/ha/method/fail", 403); + testResourceFetch("/v1/ha/class/fail", 403); + } + + @Test + public void testHaCheckForce() { + HighAvailabilityManager ha = getManagementContext().getHighAvailabilityManager(); + assertEquals(ha.getNodeState(), ManagementNodeState.MASTER); + testResourceForcedFetch("/v1/ha/method/ok", 200); + testResourceForcedFetch("/v1/ha/method/fail", 200); + testResourceForcedFetch("/v1/ha/class/fail", 200); + + getManagementContext().getHighAvailabilityManager().changeMode(HighAvailabilityMode.STANDBY); + assertEquals(ha.getNodeState(), ManagementNodeState.STANDBY); + + testResourceForcedFetch("/v1/ha/method/ok", 200); + testResourceForcedFetch("/v1/ha/method/fail", 200); + testResourceForcedFetch("/v1/ha/class/fail", 200); + + ((ManagementContextInternal)getManagementContext()).terminate(); + assertEquals(ha.getNodeState(), ManagementNodeState.TERMINATED); + + testResourceForcedFetch("/v1/ha/method/ok", 200); + testResourceForcedFetch("/v1/ha/method/fail", 200); + testResourceForcedFetch("/v1/ha/class/fail", 200); + } + + + private void testResourceFetch(String resourcePath, int code) { + testResourceFetch(resourcePath, false, code); + } + + private void testResourceForcedFetch(String resourcePath, int code) { + testResourceFetch(resourcePath, true, code); + } + + private void testResourceFetch(String resourcePath, boolean force, int code) { + Builder resource = client().resource(resourcePath) + .accept(MediaType.APPLICATION_JSON_TYPE); + if (force) { + resource.header(HaHotCheckResourceFilter.SKIP_CHECK_HEADER, "true"); + } + ClientResponse response = resource + .get(ClientResponse.class); + assertEquals(response.getStatus(), code); + } + +}