blob: a7fcb2d96558df97a3df1b3a6651e33c3559a547 [file] [log] [blame]
/*
* 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.annotation.Nullable;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ContextResolver;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.gson.Gson;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
import org.apache.brooklyn.core.config.Sanitizer;
import org.apache.brooklyn.core.config.render.RendererHints;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.resolve.jackson.BeanWithTypeUtils;
import org.apache.brooklyn.rest.domain.ApiError;
import org.apache.brooklyn.rest.transform.TaskTransformer;
import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
import org.apache.brooklyn.rest.util.DefaultExceptionMapper;
import org.apache.brooklyn.rest.util.ManagementContextProvider;
import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Boxing;
import org.apache.brooklyn.util.text.StringEscapes;
import org.apache.brooklyn.util.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
public abstract class AbstractBrooklynRestResource {
protected @Context UriInfo ui;
@Context
private ContextResolver<ManagementContext> mgmt;
private BrooklynRestResourceUtils brooklynRestResourceUtils;
private ObjectMapper mapper;
public ManagementContext mgmt() {
return Preconditions.checkNotNull(mgmt.getContext(ManagementContext.class), "mgmt");
}
public ManagementContextInternal mgmtInternal() {
return (ManagementContextInternal) mgmt();
}
protected synchronized Maybe<ManagementContext> mgmtMaybe() {
return Maybe.of(mgmt());
}
@VisibleForTesting
public void setManagementContext(ManagementContext managementContext) {
this.mgmt = new ManagementContextProvider(managementContext);
}
public synchronized BrooklynRestResourceUtils brooklyn() {
if (brooklynRestResourceUtils!=null) return brooklynRestResourceUtils;
brooklynRestResourceUtils = new BrooklynRestResourceUtils(mgmt());
return brooklynRestResourceUtils;
}
/** returns a bad request Response wrapping the given exception */
protected Response badRequest(Exception e) {
DefaultExceptionMapper.logExceptionDetailsForDebugging(e);
return ApiError.of(e).asBadRequestResponseJson();
}
protected ObjectMapper mapper() {
return mapper(mgmt());
}
protected ObjectMapper mapper(ManagementContext mgmt) {
if (mapper==null)
mapper = BrooklynJacksonJsonProvider.findAnyObjectMapper(mgmt);
return mapper;
}
protected RestValueResolver resolving(Object v) {
return resolving(v, mgmt());
}
protected RestValueResolver resolving(Object v, ManagementContext mgmt) {
return RestValueResolver.resolving(mgmt, v).mapper(mapper(mgmt));
}
public static class RestValueResolver {
final private Object valueToResolve;
ManagementContext mgmt;
private @Nullable ObjectMapper mapperN;
private boolean preferJson;
private boolean isJerseyReturnValue;
private @Nullable Entity entity;
private @Nullable Duration timeout;
private @Nullable Object rendererHintSource;
private @Nullable Boolean immediately;
private @Nullable Boolean raw;
private @Nullable Boolean useDisplayHints;
private @Nullable Boolean skipResolution;
private @Nullable Boolean suppressBecauseSecret;
private @Nullable Boolean suppressSecrets;
private boolean filterOutputFields = false;
public static RestValueResolver resolving(ManagementContext mgmt, Object v) { return new RestValueResolver(mgmt, v); }
private RestValueResolver(ManagementContext mgmt, Object v) { this.mgmt = mgmt; valueToResolve = v; }
public RestValueResolver mapper(ObjectMapper mapper) { this.mapperN = mapper; return this; }
public RestValueResolver newInstanceResolving(Object v) {
RestValueResolver result = resolving(mgmt, v);
result.mapperN = mapperN;
result.preferJson = preferJson;
result.isJerseyReturnValue = isJerseyReturnValue;
result.entity = entity;
result.timeout = timeout;
result.rendererHintSource = rendererHintSource;
result.immediately = immediately;
result.raw = raw;
result.useDisplayHints = useDisplayHints;
result.skipResolution = skipResolution;
result.suppressBecauseSecret = suppressBecauseSecret;
result.suppressSecrets = suppressSecrets;
return result;
}
public ObjectMapper mapper() {
if (mapperN==null) mapperN = BrooklynJacksonJsonProvider.findAnyObjectMapper(mgmt);
return mapperN;
}
/** whether JSON is the ultimate product;
* main effect here is to give null for null if true, else to give empty string
* <p>
* conversion to JSON for complex types is done subsequently (often by the framework)
* <p>
* default is true */
public RestValueResolver preferJson(boolean preferJson) { this.preferJson = preferJson; return this; }
/** whether an outermost string must be wrapped in quotes, because a String return object is treated as
* already JSON-encoded
* <p>
* default is false */
public RestValueResolver asJerseyOutermostReturnValue(boolean asJerseyReturnJson) {
isJerseyReturnValue = asJerseyReturnJson;
return this;
}
@Deprecated // since 1.0
public RestValueResolver raw(Boolean raw) {
this.raw = raw;
return this;
}
public RestValueResolver useDisplayHints(Boolean useDisplayHints) {
this.useDisplayHints = useDisplayHints;
return this;
}
private boolean isUseDisplayHints() {
if (raw!=null) {
if (raw) {
// explicit non-default value takes precedence
// (REST API will not allow 'null')
return !raw;
}
// otherwise pass through
}
if (useDisplayHints!=null) return useDisplayHints;
return true;
}
public boolean isSuppressedBecauseSecret() {
return Boolean.TRUE.equals(suppressBecauseSecret);
}
public RestValueResolver skipResolution(Boolean skipResolution) {
this.skipResolution = skipResolution;
return this;
}
public RestValueResolver suppressIfSecret(String keyName, Boolean suppressIfSecret) {
suppressSecrets = suppressIfSecret;
if (Boolean.TRUE.equals(suppressIfSecret)) {
if (Sanitizer.IS_SECRET_PREDICATE.apply(keyName)) {
suppressBecauseSecret = true;
}
} else {
checkAndGetSecretsSuppressed(mgmt, suppressIfSecret, null);
}
return this;
}
public RestValueResolver context(Entity entity) { this.entity = entity; return this; }
public RestValueResolver timeout(Duration timeout) { this.timeout = timeout; return this; }
public RestValueResolver immediately(boolean immediately) { this.immediately = immediately; return this; }
public RestValueResolver renderAs(Object rendererHintSource) { this.rendererHintSource = rendererHintSource; return this; }
public RestValueResolver filterOutputFields(boolean filterOutputFields) { this.filterOutputFields = filterOutputFields; return this; }
public Object resolve() {
Object valueResult =
Boolean.TRUE.equals(skipResolution)
? valueToResolve
: getImmediateValue(valueToResolve, entity, immediately, timeout);
if (valueResult==UNRESOLVED) valueResult = valueToResolve;
if (rendererHintSource!=null && isUseDisplayHints()) {
valueResult = RendererHints.applyDisplayValueHintUnchecked(rendererHintSource, valueResult);
}
if (Boolean.TRUE.equals(suppressBecauseSecret)) {
valueResult = suppressAsMinimalizedJson(mapper(), valueResult);
}
return getValueForDisplaySanitized(mgmt, mapper(), valueResult, preferJson, isJerseyReturnValue,
Boolean.TRUE.equals(suppressSecrets) && !Boolean.TRUE.equals(suppressBecauseSecret), filterOutputFields);
}
private static Object UNRESOLVED = "UNRESOLVED".toCharArray();
private static Object getImmediateValue(Object value, @Nullable Entity context, @Nullable Boolean immediately, @Nullable Duration timeout) {
return Tasks.resolving(value)
.as(Object.class)
.defaultValue(UNRESOLVED)
.deep()
.timeout(timeout)
.immediately(immediately == null ? false : immediately.booleanValue())
.context(context)
.swallowExceptions()
.get();
}
public Object getValueForDisplay(Object value, Boolean preferJson, Boolean isJerseyReturnValue, Boolean suppressNestedSecrets) {
return getValueForDisplay(mgmt, mapper(), value,
preferJson!=null ? preferJson : this.preferJson, isJerseyReturnValue!=null ? isJerseyReturnValue : this.isJerseyReturnValue,
suppressNestedSecrets!=null ? suppressNestedSecrets : this.suppressSecrets, false);
}
public Object getValueForDisplay(Object value, Boolean preferJson, Boolean isJerseyReturnValue, Boolean suppressNestedSecrets, boolean filterOutputFields) {
return getValueForDisplay(mgmt, mapper(), value,
preferJson!=null ? preferJson : this.preferJson, isJerseyReturnValue!=null ? isJerseyReturnValue : this.isJerseyReturnValue,
suppressNestedSecrets!=null ? suppressNestedSecrets : this.suppressSecrets, filterOutputFields);
}
public static Object getValueForDisplay(ManagementContext mgmt, ObjectMapper mapper, Object value, boolean preferJson, boolean isJerseyReturnValue, Boolean suppressNestedSecrets, Boolean filterOutputFields) {
suppressNestedSecrets = checkAndGetSecretsSuppressed(mgmt, suppressNestedSecrets, false);
return getValueForDisplaySanitized(mgmt, mapper, value, preferJson, isJerseyReturnValue, suppressNestedSecrets, filterOutputFields);
}
static Object getValueForDisplaySanitized(ManagementContext mgmt, ObjectMapper mapper, Object value, boolean preferJson, boolean isJerseyReturnValue, Boolean suppressNestedSecrets, Boolean filterOutputFields) {
if (suppressNestedSecrets==null) suppressNestedSecrets = false;
if (filterOutputFields==null) filterOutputFields = false;
if (preferJson) {
if (value==null) return null;
Object result = value;
// no serialization checks required, with new smart-mapper which does toString
// (note there is more sophisticated logic in git history however)
result = value;
if (result instanceof BrooklynDslDeferredSupplier) {
result = result.toString();
}
if (isJerseyReturnValue) {
if (result instanceof String) {
// Jersey does not do json encoding if the return type is a string,
// expecting the returner to do the json encoding himself
// cf discussion at https://github.com/dropwizard/dropwizard/issues/231
result = StringEscapes.JavaStringEscapes.wrapJavaString((String)result);
}
}
if (suppressNestedSecrets || filterOutputFields) {
if (result==null || Boxing.isPrimitiveOrBoxedObject(result)) {
// no action needed
} else if (result instanceof CharSequence) {
if (suppressNestedSecrets) {
result = Sanitizer.sanitizeMultilineString(result.toString());
}
} else {
// go ahead and convert to json and suppress deep
try {
String resultS = mapper.writeValueAsString(result);
result = BeanWithTypeUtils.newSimpleMapper().readValue(resultS, Object.class);
if (filterOutputFields) {
result = TaskTransformer.filterOutputFields(result);
}
if (suppressNestedSecrets) {
//the below treats all numbers as doubles
//new Gson().fromJson(resultS, Object.class);
result = Sanitizer.suppressNestedSecretsJson(result, true);
}
} catch (JsonProcessingException e) {
throw Exceptions.propagateAnnotated("Cannot serialize REST result", e);
}
}
}
return result;
} else {
if (value==null) return "";
String resultS = value.toString();
if (suppressNestedSecrets) {
if (Sanitizer.IS_SECRET_PREDICATE.apply(resultS)) {
return suppressAsMinimalizedJson(mapper, value);
}
}
return resultS;
}
}
public static Boolean checkAndGetSecretsSuppressed(ManagementContext mgmt, Boolean suppressNestedSecrets, Boolean defaultValue) {
if (Boolean.TRUE.equals(suppressNestedSecrets)) {
return true;
}
boolean areSecretsAllowed = true; // TODO check in mgmt context if secrets are allowed (change this from static to get mgmt context!)
if (!areSecretsAllowed) {
// could throw, but might want API to default to blocking
// if (Boolean.FALSE.equals(suppressNestedSecrets)) throwWebApplicationException(Response.Status.FORBIDDEN, "Not permitted to prevent suppression of secrets");
return true;
}
boolean isDefaultsSecretSAllowed = true; // TODO check in mgmt context if secrets should default to being shown in API
if (!isDefaultsSecretSAllowed) return true;
if (suppressNestedSecrets==null) return defaultValue;
return suppressNestedSecrets;
}
public static String suppressAsMinimalizedJson(ObjectMapper mapper, Object valueResult) {
try {
Object resultJ;
if (valueResult==null) valueResult = ""; // treat null as empty string
if (valueResult instanceof String) {
// don't wrap strings
resultJ = valueResult;
} else {
String resultS = mapper.writeValueAsString(valueResult);
resultJ = new Gson().fromJson(resultS, Object.class);
}
return Sanitizer.suppressJson(resultJ, true);
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
}
}