org.hyperledger.fabric.contract.metadata.MetadataBuilder Maven / Gradle / Ivy
/*
* Copyright 2019 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.fabric.contract.metadata;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.everit.json.schema.Schema;
import org.everit.json.schema.ValidationException;
import org.everit.json.schema.loader.SchemaClient;
import org.everit.json.schema.loader.SchemaLoader;
import org.everit.json.schema.loader.internal.DefaultSchemaClient;
import org.hyperledger.fabric.Logger;
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Info;
import org.hyperledger.fabric.contract.routing.ContractDefinition;
import org.hyperledger.fabric.contract.routing.DataTypeDefinition;
import org.hyperledger.fabric.contract.routing.RoutingRegistry;
import org.hyperledger.fabric.contract.routing.TransactionType;
import org.hyperledger.fabric.contract.routing.TxFunction;
import org.hyperledger.fabric.contract.routing.TypeRegistry;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
* Builder to assist in production of the metadata.
*
* This class is used to build up the JSON structure to be returned as the
* metadata It is not a store of information, rather a set of functional data to
* process to and from metadata json to the internal data structure
*/
public final class MetadataBuilder {
private static Logger logger = Logger.getLogger(MetadataBuilder.class);
private MetadataBuilder() {
}
@SuppressWarnings("serial")
static class MetadataMap extends HashMap {
V putIfNotNull(final K key, final V value) {
logger.info(key + " " + value);
if (value != null && !value.toString().isEmpty()) {
return put(key, value);
} else {
return null;
}
}
}
// Metadata is composed of three primary sections
// each of which is stored in a map
private static Map> contractMap = new HashMap<>();
private static Map overallInfoMap = new HashMap();
private static Map componentMap = new HashMap();
// The schema client used to load any other referenced schemas
private static SchemaClient schemaClient = new DefaultSchemaClient();
/**
* Validation method.
*
* @throws ValidationException if the metadata is not valid
*/
public static void validate() {
logger.info("Running schema test validation");
final ClassLoader cl = MetadataBuilder.class.getClassLoader();
try (InputStream contractSchemaInputStream = cl.getResourceAsStream("contract-schema.json");
InputStream jsonSchemaInputStream = cl.getResourceAsStream("json-schema-draft-04-schema.json")) {
final JSONObject rawContractSchema = new JSONObject(new JSONTokener(contractSchemaInputStream));
final JSONObject rawJsonSchema = new JSONObject(new JSONTokener(jsonSchemaInputStream));
final SchemaLoader schemaLoader = SchemaLoader.builder().schemaClient(schemaClient).schemaJson(rawContractSchema)
.registerSchemaByURI(URI.create("http://json-schema.org/draft-04/schema"), rawJsonSchema).build();
final Schema schema = schemaLoader.load().build();
schema.validate(metadata());
} catch (final IOException e) {
throw new RuntimeException(e);
} catch (final ValidationException e) {
logger.error(e.getMessage());
e.getCausingExceptions().stream().map(ValidationException::getMessage).forEach(logger::info);
logger.error(debugString());
throw e;
}
}
/**
* Setup the metadata from the found contracts.
*
* @param registry RoutingRegistry
* @param typeRegistry TypeRegistry
*/
public static void initialize(final RoutingRegistry registry, final TypeRegistry typeRegistry) {
final Collection contractDefinitions = registry.getAllDefinitions();
contractDefinitions.forEach(MetadataBuilder::addContract);
final Collection dataTypes = typeRegistry.getAllDataTypes();
dataTypes.forEach(MetadataBuilder::addComponent);
// need to validate that the metadata that has been created is really valid
// it should be as it's been created by code, but this is a valuable double
// check
logger.info("Validating schema created");
MetadataBuilder.validate();
}
/**
* Adds a component/ complex data-type.
*
* @param datatype DataTypeDefinition
*/
public static void addComponent(final DataTypeDefinition datatype) {
final Map component = new HashMap<>();
component.put("$id", datatype.getName());
component.put("type", "object");
component.put("additionalProperties", false);
final Map propertiesMap = datatype.getProperties().entrySet().stream()
.collect(Collectors.toMap(Entry::getKey, e -> (e.getValue().getSchema())));
component.put("properties", propertiesMap);
componentMap.put(datatype.getSimpleName(), component);
}
/**
* Adds a new contract to the metadata as represented by the class object.
*
* @param contractDefinition Class of the object to use as a contract
* @return the key that the contract class is referred to in the metadata
*/
@SuppressWarnings("serial")
public static String addContract(final ContractDefinition contractDefinition) {
final String key = contractDefinition.getName();
final Contract annotation = contractDefinition.getAnnotation();
final Info info = annotation.info();
final HashMap infoMap = new HashMap();
infoMap.put("title", info.title());
infoMap.put("description", info.description());
infoMap.put("termsOfService", info.termsOfService());
infoMap.put("contact", new MetadataMap() {
{
putIfNotNull("email", info.contact().email());
putIfNotNull("name", info.contact().name());
putIfNotNull("url", info.contact().url());
}
});
infoMap.put("license", new MetadataMap() {
{
put("name", info.license().name());
putIfNotNull("url", info.license().url());
}
});
infoMap.put("version", info.version());
final HashMap contract = new HashMap();
contract.put("name", key);
contract.put("transactions", new ArrayList