
org.hyperledger.fabric.contract.metadata.MetadataBuilder Maven / Gradle / Ivy
Show all versions of fabric-chaincode-shim Show documentation
/*
* 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.io.UncheckedIOException;
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
*/
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public final class MetadataBuilder {
private static final Logger LOGGER = Logger.getLogger(MetadataBuilder.class);
private static final int PADDING = 3;
// 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();
static final class MetadataMap extends HashMap {
private static final long serialVersionUID = 1L;
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;
}
}
}
private MetadataBuilder() {}
/**
* 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 UncheckedIOException(e);
} catch (final ValidationException e) {
LOGGER.error(e::getMessage);
e.getCausingExceptions().stream()
.map(ValidationException::getMessage)
.forEach(LOGGER::info);
LOGGER.error(MetadataBuilder::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");
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("PMD.LooseCoupling")
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());
MetadataMap contact = new MetadataMap<>();
contact.putIfNotNull("email", info.contact().email());
contact.putIfNotNull("name", info.contact().name());
contact.putIfNotNull("url", info.contact().url());
infoMap.put("contact", contact);
MetadataMap license = new MetadataMap<>();
license.put("name", info.license().name());
license.putIfNotNull("url", info.license().url());
infoMap.put("license", license);
infoMap.put("version", info.version());
final HashMap contract = new HashMap<>();
contract.put("name", key);
contract.put("transactions", new ArrayList<>());
contract.put("info", infoMap);
contractMap.put(key, contract);
overallInfoMap.putAll(infoMap);
final Collection fns = contractDefinition.getTxFunctions();
fns.forEach(txFn -> addTransaction(txFn, key));
return key;
}
/**
* Adds a new transaction function to the metadata for the given contract.
*
* @param txFunction Object representing the transaction function
* @param contractName Name of the contract that this function belongs to
*/
public static void addTransaction(final TxFunction txFunction, final String contractName) {
final TypeSchema transaction = new TypeSchema();
final TypeSchema returnSchema = txFunction.getReturnSchema();
if (returnSchema != null) {
transaction.put("returns", returnSchema);
}
final List tags = new ArrayList<>();
tags.add(txFunction.getType());
if (txFunction.getType() == TransactionType.SUBMIT) { // add deprecated tags
tags.add(TransactionType.INVOKE);
} else {
tags.add(TransactionType.QUERY);
}
final Map contract = contractMap.get(contractName);
@SuppressWarnings("unchecked")
final List