/*
* Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.hawkular.inventory.base;
import static java.util.Collections.emptyList;
import static org.hawkular.inventory.api.Relationships.WellKnown.contains;
import static org.hawkular.inventory.api.Relationships.WellKnown.defines;
import static org.hawkular.inventory.api.Relationships.WellKnown.hasData;
import static org.hawkular.inventory.api.filters.With.id;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.hawkular.inventory.api.Data;
import org.hawkular.inventory.api.EntityNotFoundException;
import org.hawkular.inventory.api.Query;
import org.hawkular.inventory.api.ValidationException;
import org.hawkular.inventory.api.ValidationException.ValidationMessage;
import org.hawkular.inventory.api.filters.Filter;
import org.hawkular.inventory.api.filters.Related;
import org.hawkular.inventory.api.filters.With;
import org.hawkular.inventory.api.model.DataEntity;
import org.hawkular.inventory.api.model.StructuredData;
import org.hawkular.inventory.api.paging.Page;
import org.hawkular.inventory.api.paging.Pager;
import org.hawkular.inventory.base.spi.Discriminator;
import org.hawkular.inventory.base.spi.ShallowStructuredData;
import org.hawkular.inventory.paths.CanonicalPath;
import org.hawkular.inventory.paths.DataRole;
import org.hawkular.inventory.paths.RelativePath;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.fge.jackson.JsonNodeReader;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ListReportProvider;
import com.github.fge.jsonschema.core.report.LogLevel;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.github.fge.jsonschema.main.JsonValidator;
/**
* Contains access interface implementations for accessing data entities.
*
* @author Lukas Krejci
* @since 0.3.0
*/
public final class BaseData {
private BaseData() {
}
public static final class Read<BE, R extends DataRole> extends Traversal<BE, DataEntity>
implements Data.Read<R> {
private final DataModificationChecks<BE> checks;
public Read(TraversalContext<BE, DataEntity> context, DataModificationChecks<BE> checks) {
super(context);
this.checks = checks;
}
@Override
public Data.Multiple getAll(Filter[][] filters) {
return new Multiple<>(context.proceed().whereAll(filters).get());
}
@Override
public Data.Single get(R role) throws EntityNotFoundException {
return new Single<>(context.proceed().where(id(role.name())).get(), checks);
}
}
public static final class ReadWrite<BE, R extends DataRole>
extends Mutator<BE, DataEntity, DataEntity.Blueprint<R>, DataEntity.Update, R>
implements Data.ReadWrite<R> {
private final DataModificationChecks<BE> checks;
private final Class<R> dataRoleClass;
public ReadWrite(TraversalContext<BE, DataEntity> context, Class<R> dataRoleClass,
DataModificationChecks<BE> checks) {
super(context);
this.dataRoleClass = dataRoleClass;
this.checks = checks;
}
@Override
protected String getProposedId(Transaction<BE> tx, DataEntity.Blueprint<R> blueprint) {
if (!dataRoleClass.equals(blueprint.getRole().getClass())) {
throw new IllegalArgumentException("Invalid role/id. Admissible values for this data position are: "
+ Arrays.asList(dataRoleClass.getEnumConstants()).stream().map(DataRole::name));
}
return blueprint.getRole().name();
}
@Override
protected EntityAndPendingNotifications<BE, DataEntity> wireUpNewEntity(Discriminator discriminator, BE entity,
DataEntity.Blueprint<R> blueprint,
CanonicalPath parentPath, BE parent,
Transaction<BE> tx) {
Validator.validate(discriminator, tx, blueprint.getValue(), entity);
BE value = tx.persist(blueprint.getValue());
//don't report this relationship, it is implicit
//also, don't run the RelationshipRules checks - we're in the "privileged code" that is allowed to do
//this
tx.relate(discriminator, entity, value, hasData.name(), null);
DataEntity data = new DataEntity(parentPath, blueprint.getRole(), blueprint.getValue(), null,
null, null, blueprint.getProperties());
return new EntityAndPendingNotifications<>(entity, data, emptyList());
}
@Override
public Data.Single create(DataEntity.Blueprint<R> data, boolean cache) {
return new Single<>(context.toCreatedEntity(doCreate(data), cache), checks);
}
@Override
protected void preCreate(DataEntity.Blueprint<R> blueprint, Transaction<BE> transaction) {
preCreate(checks, blueprint, transaction);
}
@Override
protected void postCreate(BE entityObject, DataEntity entity, Transaction<BE> transaction) {
postCreate(checks, entityObject, transaction);
}
@Override
protected void preDelete(R role, BE entityRepresentation, Transaction<BE> transaction) {
preDelete(context.discriminator(), checks, entityRepresentation, transaction);
}
@Override protected void postDelete(BE entityRepresentation, Transaction<BE> transaction) {
postDelete(checks, entityRepresentation, transaction);
}
@Override
protected void preUpdate(R role, BE entityRepresentation, DataEntity.Update update,
Transaction<BE> transaction) {
preUpdate(context.discriminator(), checks, entityRepresentation, update, transaction);
}
@Override
protected void postUpdate(BE entityRepresentation, Transaction<BE> transaction) {
postUpdate(checks, entityRepresentation, transaction);
}
@Override
public Data.Multiple getAll(Filter[][] filters) {
return new Multiple<>(context.proceed().whereAll(filters).get());
}
@Override
public Data.Single get(R role) throws EntityNotFoundException {
return new Single<>(context.proceed().where(id(role.name())).get(), checks);
}
private static <BE, R extends DataRole>
void preCreate(DataModificationChecks<BE> checks, DataEntity.Blueprint<R> blueprint,
Transaction<BE> transaction) {
checks.preCreate(blueprint, transaction);
}
private static <BE> void postCreate(DataModificationChecks<BE> checks, BE entity,
Transaction<BE> transaction) {
checks.postCreate(entity, transaction);
}
private static <BE> void preUpdate(Discriminator discriminator, DataModificationChecks<BE> checks, BE entityRepresentation,
DataEntity.Update update, Transaction<BE> transaction) {
checks.preUpdate(entityRepresentation, update, transaction);
Validator.validate(discriminator, transaction, update.getValue(), entityRepresentation);
}
private static <BE> void postUpdate(DataModificationChecks<BE> checks, BE entity,
Transaction<BE> transaction) {
checks.postCreate(entity, transaction);
}
private static <BE> void preDelete(Discriminator discriminator, DataModificationChecks<BE> checks, BE entityRepresentation,
Transaction<BE> tx) {
checks.preDelete(entityRepresentation, tx);
}
private static <BE> void postDelete(DataModificationChecks<BE> checks, BE entity,
Transaction<BE> transaction) {
checks.postDelete(entity, transaction);
}
}
public static final class Single<BE> extends SingleSyncedFetcher<BE, DataEntity, DataEntity.Blueprint<?>,
DataEntity.Update> implements Data.Single {
private final DataModificationChecks<BE> checks;
public Single(TraversalContext<BE, DataEntity> context, DataModificationChecks<BE> checks) {
super(context);
this.checks = checks;
}
@Override
public StructuredData data(RelativePath dataPath) {
//doing this in 2 queries might seem inefficient but this I think needs to be done to be able to
//do the filtering
return loadEntity((b, e, tx) -> {
BE dataEntity = tx.descendToData(context.discriminator(), b, dataPath);
return dataEntity == null ? null : tx.convert(context.discriminator(), dataEntity, StructuredData.class);
});
}
@Override
public StructuredData flatData(RelativePath dataPath) {
return loadEntity((b, e, tx) -> {
BE dataEntity = tx.descendToData(context.discriminator(), b, dataPath);
return dataEntity == null ? null : tx.convert(context.discriminator(), dataEntity, ShallowStructuredData.class)
.getData();
});
}
@Override
protected void preDelete(BE deletedEntity, Transaction<BE> transaction) {
ReadWrite.preDelete(context.discriminator(), checks, deletedEntity, transaction);
}
@Override
protected void postDelete(BE deletedEntity, Transaction<BE> transaction) {
ReadWrite.postDelete(checks, deletedEntity, transaction);
}
@Override
protected void preUpdate(BE updatedEntity, DataEntity.Update update, Transaction<BE> t) {
ReadWrite.preUpdate(context.discriminator(), checks, updatedEntity, update, t);
}
@Override
protected void postUpdate(BE updatedEntity, Transaction<BE> transaction) {
ReadWrite.postUpdate(checks, updatedEntity, transaction);
}
}
public static final class Multiple<BE>
extends MultipleEntityFetcher<BE, DataEntity, DataEntity.Update>
implements Data.Multiple {
public Multiple(TraversalContext<BE, DataEntity> context) {
super(context);
}
@Override
public Page<StructuredData> data(RelativePath dataPath, Pager pager) {
return loadEntities(pager, (b, e, tx) -> {
BE dataEntity = tx.descendToData(context.discriminator(), b, dataPath);
return tx.convert(context.discriminator(), dataEntity, StructuredData.class);
});
}
@Override
public Page<StructuredData> flatData(RelativePath dataPath, Pager pager) {
return loadEntities(pager, (b, e, tx) -> {
BE dataEntity = tx.descendToData(context.discriminator(), b, dataPath);
return tx.convert(context.discriminator(), dataEntity, ShallowStructuredData.class).getData();
});
}
}
public static final class Validator {
private static final JsonValidator VALIDATOR = JsonSchemaFactory.newBuilder()
.setReportProvider(new ListReportProvider(LogLevel.INFO, LogLevel.FATAL)).freeze().getValidator();
private static Filter[] navigateToSchema(DataRole role) {
if (role == DataRole.Resource.configuration) {
return new Filter[]{
//up to the containing resource
Related.asTargetBy(contains),
//up to the defining resource type
Related.asTargetBy(defines),
//down to the contained data entity
Related.by(contains), With.type(DataEntity.class),
//with id of configuration schema
With.id(DataRole.ResourceType.configurationSchema.name())
};
} else if (role == DataRole.Resource.connectionConfiguration) {
return new Filter[]{
//up to the containing resource
Related.asTargetBy(contains),
//up to the defining resource type
Related.asTargetBy(defines),
//down to the contained data entity
Related.by(contains), With.type(DataEntity.class),
//with id of configuration schema
With.id(DataRole.ResourceType.connectionConfigurationSchema.name())
};
} else {
throw new IllegalStateException("Incomplete mapping of navigation to data schema. Role '" + role + "'" +
" is not handled.");
}
}
public static <BE> void validate(Discriminator discriminator, Transaction<BE> tx, StructuredData data, BE dataEntity) {
CanonicalPath path = tx.extractCanonicalPath(dataEntity);
DataRole role = DataRole.valueOf(path.ids().getDataRole());
if (role.isSchema()) {
try {
JsonNode schema = new JsonNodeReader(new ObjectMapper())
.fromInputStream(BaseData.class.getResourceAsStream("/json-meta-schema.json"));
CanonicalPath dataPath = tx.extractCanonicalPath(dataEntity);
validate(dataPath, convert(data), schema);
} catch (IOException e) {
throw new IllegalStateException("Could not load the embedded JSON Schema meta-schema.");
}
} else {
validateIfSchemaFound(discriminator, tx, data, dataEntity, Query.path().with(navigateToSchema(role)).get());
}
}
private static <BE> void validateIfSchemaFound(Discriminator discriminator, Transaction<BE> tx, StructuredData data,
BE dataEntity, Query query) {
BE possibleSchema = tx.traverseToSingle(discriminator, dataEntity, query);
if (possibleSchema == null) {
//no schema means anything is OK
return;
}
DataEntity schemaEntity = tx.convert(discriminator, possibleSchema, DataEntity.class);
CanonicalPath dataPath = tx.extractCanonicalPath(dataEntity);
validate(dataPath, convert(data), convert(schemaEntity.getValue()));
}
private static void validate(CanonicalPath dataPath, JsonNode dataNode, JsonNode schemaNode) {
//explicitly allow null schemas
if (dataNode == null || dataNode.isNull()) {
return;
}
try {
ProcessingReport report = VALIDATOR.validate(schemaNode, dataNode, true);
if (!report.isSuccess()) {
List<ValidationMessage> messages = new ArrayList<>();
report.forEach((m) ->
messages.add(new ValidationMessage(m.getLogLevel().name(), m.toString())));
throw new ValidationException(dataPath, messages, null);
}
} catch (ProcessingException e) {
throw new ValidationException(dataPath, emptyList(), e);
}
}
private static JsonNode convert(StructuredData data) {
return data.accept(new StructuredData.Visitor.Simple<JsonNode, Void>() {
@Override
public JsonNode visitBool(boolean value, Void ignored) {
return JsonNodeFactory.instance.booleanNode(value);
}
@Override
public JsonNode visitFloatingPoint(double value, Void ignored) {
return JsonNodeFactory.instance.numberNode(value);
}
@Override
public JsonNode visitIntegral(long value, Void ignored) {
return JsonNodeFactory.instance.numberNode(value);
}
@Override
public JsonNode visitList(List<StructuredData> value, Void ignored) {
ArrayNode list = JsonNodeFactory.instance.arrayNode();
value.forEach((s) -> list.add(s.accept(this, null)));
return list;
}
@Override
public JsonNode visitMap(Map<String, StructuredData> value, Void ignored) {
ObjectNode object = JsonNodeFactory.instance.objectNode();
value.forEach((k, v) -> object.set(k, v.accept(this, null)));
return object;
}
@Override
public JsonNode visitString(String value, Void ignored) {
return JsonNodeFactory.instance.textNode(value);
}
@Override
public JsonNode visitUndefined(Void ignored) {
return JsonNodeFactory.instance.nullNode();
}
}, null);
}
}
public interface DataModificationChecks<BE> {
static <BE> DataModificationChecks<BE> none() {
return new DataModificationChecks<BE>() {
};
}
default void preCreate(DataEntity.Blueprint blueprint, Transaction<BE> transaction) {
}
default void postCreate(BE dataEntity, Transaction<BE> transaction) {
}
default void preUpdate(BE dataEntity, DataEntity.Update update, Transaction<BE> transaction) {
}
default void postUpdate(BE dataEntity, Transaction<BE> transaction) {
}
default void preDelete(BE dataEntity, Transaction<BE> transaction) {
}
default void postDelete(BE dataEntity, Transaction<BE> transaction) {
}
}
}