UNPKG

@asyncapi/java-spring-cloud-stream-template

Version:

Java Spring Cloud Stream template for AsyncAPI generator.

1,085 lines (948 loc) 36.8 kB
const filter = module.exports; const yaml = require('js-yaml'); const generatorFilters = require('@asyncapi/generator-filters'); const _ = require('lodash'); const ScsLib = require('../lib/scsLib.js'); const scsLib = new ScsLib(); const ApplicationModel = require('../lib/applicationModel.js'); const applicationModel = new ApplicationModel('all'); // To enable debug logging, set the env var DEBUG="type function" with whatever things you want to see. const debugDynamic = require('debug')('dynamic'); const debugFunction = require('debug')('function'); const debugJavaClass = require('debug')('javaClass'); const debugPayload = require('debug')('payload'); const debugProperty = require('debug')('property'); const debugChannel = require('debug')('channel'); const debugType = require('debug')('type'); const stringMap = new Map(); stringMap.set('date',{javaType: 'java.time.LocalDate', printFormat: '%s', sample: '2000-12-31'}); stringMap.set('date-time',{javaType: 'java.time.OffsetDateTime', printFormat: '%s', sample: '2000-12-31T23:59:59+01:00'}); stringMap.set('byte',{javaType: 'byte[]', printFormat: '%s', sample: 'U3dhZ2dlciByb2Nrcw=='}); stringMap.set('binary',{javaType: 'byte[]', printFormat: '%s', sample: 'base64-encoded file contents'}); stringMap.set(undefined,{javaType: 'String', printFormat: '%s', sample: '"string"'}); const integerMap = new Map(); integerMap.set('int32',{javaType: 'Integer', printFormat: '%d', sample: '1'}); integerMap.set('int64',{javaType: 'Long', printFormat: '%d', sample: '1L'}); integerMap.set(undefined,{javaType: 'Integer', printFormat: '%d', sample: '1'}); const numberMap = new Map(); numberMap.set('float',{javaType: 'Float', printFormat: '%f', sample: '1.1F'}); numberMap.set('double',{javaType: 'Double', printFormat: '%f', sample: '1.1'}); numberMap.set(undefined,{javaType: 'java.math.BigDecimal', printFormat: '%s', sample: '100.1'}); const booleanMap = new Map(); booleanMap.set(undefined,{javaType: 'Boolean', printFormat: '%s', sample: 'true'}); const nullMap = new Map(); nullMap.set(undefined,{javaType: 'String', printFormat: '%s', sample: 'null'}); const typeMap = new Map(); typeMap.set('boolean', booleanMap); typeMap.set('integer', integerMap); typeMap.set('null', nullMap); typeMap.set('number', numberMap); typeMap.set('string', stringMap); function getType(type, format) { let typeObject = typeMap.get(type).get(format); if (typeObject === undefined) { typeObject = typeMap.get(type).get(undefined); } return typeObject; } class SCSFunction { get isPublisher() { return this.type === 'function' || this.type === 'supplier'; } get isSubscriber() { return this.type === 'function' || this.type === 'consumer'; } get publishBindingName() { return `${this.name }-out-0`; } get subscribeBindingName() { return `${this.name }-in-0`; } // This returns true if this is a function, but we need to render it as a consumer // because we need to call streamBridge to send out the message. get functionAsConsumer() { return this.type === 'function' && this.dynamic && this.dynamicType === 'streamBridge'; } get functionSignature() { let ret = ''; if (this.type === 'consumer' || (this.type === 'function' && this.dynamic && this.dynamicType === 'streamBridge')) { if (this.reactive) { ret = `public Consumer<Flux<${this.subscribePayload}>> ${this.name}()`; } else if (this.dynamic && this.parametersToHeaders) { ret = `public Consumer<Message<${this.subscribePayload}>> ${this.name}()`; } else { ret = `public Consumer<${this.subscribePayload}> ${this.name}()`; } } else if (this.type === 'supplier') { ret = this.getSupplierFunctionSignature(); } else if (this.type === 'function') { ret = this.getFunctionMethodSignature(); } else { throw new Error(`Can't determine the function signature for ${this.name} because the type is ${this.type}`); } return ret; } getSupplierFunctionSignature() { let ret = ''; if (this.dynamic) { if (this.reactive) { ret = `public Supplier<Flux<Message<${this.publishPayload}>>> ${this.name}()`; } else { ret = `public Supplier<Message<${this.publishPayload}>> ${this.name}()`; } } else if (this.reactive) { ret = `public Supplier<Flux<${this.publishPayload}>> ${this.name}()`; } else { ret = `public Supplier<${this.publishPayload}> ${this.name}()`; } return ret; } getFunctionMethodSignature() { let ret = ''; if (this.dynamic) { if (this.reactive) { ret = `public Function<Flux<${this.subscribePayload}>, Flux<Message<${this.publishPayload}>>> ${this.name}()`; } else { ret = `public Function<${this.subscribePayload}, Message<${this.publishPayload}>> ${this.name}()`; } } else if (this.reactive) { ret = `public Function<Flux<${this.subscribePayload}>, Flux<${this.publishPayload}>> ${this.name}()`; } else { ret = `public Function<${this.subscribePayload}, ${this.publishPayload}> ${this.name}()`; } return ret; } } // This generates the object that gets rendered in the application.yaml file. function appProperties([asyncapi, params]) { debugProperty('appProperties start'); if (params.binder !== 'kafka' && params.binder !== 'rabbit' && params.binder !== 'solace') { throw new Error('Please provide a parameter named \'binder\' with the value kafka, rabbit or solace.'); } const doc = {}; doc.spring = {}; doc.spring.cloud = {}; const cloud = doc.spring.cloud; cloud.function = {}; // See if we have dynamic functions, and if the parametersToHeaders param is set. // If so, add the input-header-mapping-expression config to consumers which consume dynamic channels. if (params.parametersToHeaders) { handleParametersToHeaders(asyncapi, params, cloud); } debugProperty('appProperties getFunctionDefinitions'); cloud.function.definition = getFunctionDefinitions(asyncapi, params); cloud.stream = {}; const scs = cloud.stream; debugProperty('appProperties getBindings'); scs.bindings = getBindings(asyncapi, params); if (params.binder === 'solace') { debugProperty('appProperties getAdditionalSubs'); const additionalSubs = getAdditionalSubs(asyncapi, params); if (additionalSubs) { scs.solace = additionalSubs; } } if (params.binder === 'kafka') { const brokerSettings = getBrokerSettings(asyncapi, params); if (brokerSettings) { scs.kafka = brokerSettings; } } if (isApplication(params)) { if (params.binder === 'solace') { scs.binders = {}; scs.binders['solace-binder'] = {}; const sb = scs.binders['solace-binder']; sb.type = 'solace'; sb.environment = {}; sb.environment.solace = getSolace(params); } doc.logging = {}; doc.logging.level = {}; doc.logging.level.root = 'info'; doc.logging.level.org = {}; doc.logging.level.org.springframework = 'info'; if (params.actuator === 'true') { doc.server = {}; doc.server.port = 8080; doc.management = {}; doc.management.endpoints = {}; doc.management.endpoints.web = {}; doc.management.endpoints.web.exposure = {}; doc.management.endpoints.web.exposure.include = '*'; } } const ym = yaml.safeDump(doc, { lineWidth: 200 }); debugProperty('appProperties end'); return ym; } filter.appProperties = appProperties; function handleParametersToHeaders(asyncapi, params, cloud) { const dynamicFuncs = getDynamicFunctions([asyncapi, params]); if (dynamicFuncs && (params.binder === 'solace' || params.binder === 'rabbit')) { cloud.function.configuration = {}; const funcs = getFunctionSpecs(asyncapi, params); funcs.forEach((spec, name, map) => { if (spec.dynamic && spec.type === 'consumer') { cloud.function.configuration[name] = {}; cloud.function.configuration[name]['input-header-mapping-expression'] = {}; const headerConfig = cloud.function.configuration[name]['input-header-mapping-expression']; addHeaderConfigs(params, spec.channelInfo, headerConfig); } }); } } function addHeaderConfigs(params, channelInfo, headerConfig) { for (const param of channelInfo.parameters) { if (params.binder === 'solace') { headerConfig[param.name] = `headers.solace_destination.getName.split("/")[${param.position}]`; } else if (params.binder === 'rabbit') { headerConfig[param.name] = `headers.amqp_receivedRoutingKey.getName.split("/")[${param.position}]`; } } } function artifactId([info, params]) { return scsLib.getParamOrDefault(info, params, 'artifactId', 'x-artifact-id'); } filter.artifactId = artifactId; function addtoExtraIncludesFromFunctionSpecs(asyncapi, params, extraIncludes) { const funcs = getFunctionSpecs(asyncapi, params); funcs.forEach((spec, name, map) => { if (spec.dynamic) { extraIncludes.dynamic = true; } if (spec.multipleMessages || spec.dynamic) { extraIncludes.needMessage = true; } if (spec.type === 'function' && !spec.functionAsConsumer) { extraIncludes.needBean = true; extraIncludes.needFunction = true; } if ((spec.type === 'supplier' && !(spec.dynamic && spec.dynamicType === 'streamBridge'))) { extraIncludes.needBean = true; extraIncludes.needSupplier = true; } if (spec.type === 'consumer' || spec.functionAsConsumer) { extraIncludes.needBean = true; extraIncludes.needConsumer = true; } }); } function appExtraIncludes([asyncapi, params]) { const extraIncludes = {}; for (const channelName in asyncapi.channels()) { const channel = asyncapi.channels()[channelName]; const subscribe = channel.subscribe(); if (subscribe && (subscribe.hasMultipleMessages())) { extraIncludes.needMessage = true; } const publish = channel.publish(); if (publish && publish.hasMultipleMessages()) { extraIncludes.needMessage = true; } } addtoExtraIncludesFromFunctionSpecs(asyncapi, params, extraIncludes); return extraIncludes; } filter.appExtraIncludes = appExtraIncludes; function identifierName(str) { return scsLib.getIdentifierName(str); } filter.identifierName = identifierName; function indent1(numTabs) { return indent(numTabs); } filter.indent1 = indent1; function indent2(numTabs) { return indent(numTabs + 1); } filter.indent2 = indent2; function indent3(numTabs) { return indent(numTabs + 2); } filter.indent3 = indent3; // This returns the proper Java type for a schema property. function fixType([name, javaName, property]) { debugType(`fixType: ${name}`); debugType(property); let isArrayOfObjects = false; // For message headers, type is a property. // For schema properties, type is a function. let type = property.type; let format = property.format; debugType(`fixType: ${property}`); if (typeof type === 'function') { type = property.type(); format = property.format(); } debugType(`fixType: type: ${type} javaName ${javaName}`); debugType(property); // If a schema has a property that is a ref to another schema, // the type is undefined, and the title gives the title of the referenced schema. let typeName; if (type === undefined) { if (property.enum()) { debugType('It is an enum.'); typeName = _.upperFirst(javaName); } else if (property.oneOf() || property.anyOf() || property.allOf()) { // Picking a type for the user may be difficult - especially since the first avro union value must be the default value which is normally of type null. typeName = 'Object'; } else { throw new Error(`Can't determine the type of property ${ name }`); } } else if (type === 'array') { if (!property.items()) { throw new Error(`Array named ${ name } must have an 'items' property to indicate what type the array elements are.`); } let itemsType = property.items().type(); if (itemsType) { if (itemsType === 'object') { isArrayOfObjects = true; itemsType = _.upperFirst(javaName); } else { itemsType = getType(itemsType, format).javaType; } } if (!itemsType) { itemsType = property.items().ext('x-parser-schema-id'); if (!itemsType) { throw new Error(`Array named ${ name }: can't determine the type of the items.`); } } typeName = `${itemsType}[]`; } else if (type === 'object') { typeName = _.upperFirst(javaName); } else if (property.enum()) { debugType('It is an enum.'); typeName = _.upperFirst(javaName); } else { typeName = getType(type,format).javaType; if (!typeName) { typeName = type; } } return [typeName, isArrayOfObjects]; } filter.fixType = fixType; function functionSpecs([asyncapi, params]) { return getFunctionSpecs(asyncapi, params); } filter.functionSpecs = functionSpecs; // This returns the non-SCS type functions for sending to dynamic channels. function getDynamicFunctions([asyncapi, params]) { const functionMap = new Map(); debugDynamic('start:'); for (const channelName in asyncapi.channels()) { const channel = asyncapi.channels()[channelName]; debugDynamic(`getDynamicFunctions channelName ${channelName}`); debugDynamic(channel); const publisher = scsLib.getRealPublisher(asyncapi.info(), params, channel); if (publisher) { debugDynamic('found publisher:'); debugDynamic(publisher); const channelInfo = getChannelInfo(params, channelName, channel); if (channelInfo.hasParams) { const spec = {}; spec.channelInfo = channelInfo; spec.payloadClass = getPayloadClass(publisher); spec.sendMethodName = getSendFunctionName(channelName, publisher); functionMap.set(spec.sendMethodName, spec); } } } debugDynamic('functionMap'); debugDynamic(functionMap); return functionMap; } filter.getDynamicFunctions = getDynamicFunctions; // This returns the list of methods belonging to an object, just to help debugging. const getMethods = (obj) => { const properties = new Set(); let currentObj = obj; do { Object.getOwnPropertyNames(currentObj).forEach(item => properties.add(item)); } while ((currentObj = Object.getPrototypeOf(currentObj))); return [...properties.keys()].filter(item => typeof obj[item] === 'function'); }; function getModelClass(customSchemaObject) { return applicationModel.getModelClass(customSchemaObject); } filter.getModelClass = getModelClass; function getAnonymousSchemaForRef(schemaName) { return applicationModel.getAnonymousSchemaForRef(schemaName); } filter.getAnonymousSchemaForRef = getAnonymousSchemaForRef; function getRealPublisher([info, params, channel]) { return scsLib.getRealPublisher(info, params, channel); } filter.getRealPublisher = getRealPublisher; function getRealSubscriber([info, params, channel]) { return scsLib.getRealSubscriber(info, params, channel); } filter.getRealSubscriber = getRealSubscriber; function groupId([info, params]) { return scsLib.getParamOrDefault(info, params, 'groupId', 'x-group-id'); } filter.groupId = groupId; function isEmpty(obj) { if (!obj) { return true; } return obj && Object.keys(obj).length === 0 && obj.constructor === Object; } filter.isEmpty = isEmpty; // Called from the java-class partial function logJavaClass(obj) { debugJavaClass(obj); } filter.logJavaClass = logJavaClass; function logFull(obj) { console.log(obj); if (obj) { console.log(dump(obj)); console.log(getMethods(obj)); } return obj; } filter.logFull = logFull; function lowerFirst(str) { return _.lowerFirst(str); } filter.lowerFirst = lowerFirst; function mainClassName([info, params]) { return scsLib.getParamOrDefault(info, params, 'javaClass', 'x-java-class', 'Application'); } filter.mainClassName = mainClassName; // This returns the Java class name of the payload. function payloadClass([channelName, channel]) { let ret; if (channel.publish()) { ret = getPayloadClass(channel.publish()); } if (!ret && channel.subscribe()) { ret = getPayloadClass(channel.subscribe()); } if (!ret) { throw new Error(`Channel ${ channelName }: no payload class has been defined.`); } return ret; } filter.payloadClass = payloadClass; function schemaExtraIncludes([schemaName, schema]) { debugProperty(`schemaExtraIncludes ${schemaName} ${schema.type()}`); const ret = {}; if (checkPropertyNames(schemaName, schema)) { ret.needJsonPropertyInclude = true; } debugProperty('checkPropertyNames:'); debugProperty(ret); return ret; } filter.schemaExtraIncludes = schemaExtraIncludes; function solaceSpringCloudVersion([info, params]) { return scsLib.getParamOrDefault(info, params, 'solaceSpringCloudVersion', 'x-solace-spring-cloud-version'); } filter.solaceSpringCloudVersion = solaceSpringCloudVersion; function springBootVersion([info, params]) { return scsLib.getParamOrDefault(info, params, 'springBootVersion', 'x-spring-boot-version'); } filter.springBootVersion = springBootVersion; function springCloudStreamVersion([info, params]) { return scsLib.getParamOrDefault(info, params, 'springCloudStreamVersion', 'x-spring-cloud-stream-version'); } filter.springCloudStreamVersion = springCloudStreamVersion; function springCloudVersion([info, params]) { return scsLib.getParamOrDefault(info, params, 'springCloudVersion', 'x-spring-cloud-version'); } filter.springCloudVersion = springCloudVersion; function stringify(obj) { return JSON.stringify(obj, null, 2); } filter.stringify = stringify; function extraImports([asyncapi, params]) { return getExtraImports(asyncapi); } filter.extraImports = extraImports; function getExtraImports(asyncapi) { const schemaImports = []; const channelOperationInfos = []; for (const channelName in asyncapi.channels()) { const channel = asyncapi.channels()[channelName]; if (channel.hasPublish()) { channelOperationInfos.push(channel.publish()); } if (channel.hasSubscribe()) { channelOperationInfos.push(channel.subscribe()); } } if (channelOperationInfos.length > 0) { channelOperationInfos.forEach(channelOperationInfo => { const fullPackagePath = getPayloadPackage(channelOperationInfo); if (fullPackagePath) { schemaImports.push(fullPackagePath); } }); } return schemaImports; } function getPayloadPackage(pubOrSub) { let fullPackagePath; if (!pubOrSub.hasMultipleMessages()) { let payload; if (pubOrSub.message()) { payload = pubOrSub.message().payload(); } if (payload) { const type = payload.type(); const importName = payload.ext('x-parser-schema-id'); // This is a schema within a package - like an avro schema in a namespace. We're hoping the full thing is part of the x-schema-parser-id. if ((!type || type === 'object') && importName.includes('.')) { fullPackagePath = importName; } } } return fullPackagePath; } // Returns true if any property names will be different between json and java. function checkPropertyNames(name, schema) { const ret = false; debugProperty(`checkPropertyNames: checking schema ${name}`); debugProperty(schema); let properties = schema.properties(); if (schema.type() === 'array') { properties = schema.items().properties(); } debugProperty(`schema type : ${schema.type()}`); for (const propName in properties) { const javaName = scsLib.getIdentifierName(propName); const prop = properties[propName]; debugProperty(`checking ${propName} ${prop.type()}`); if (javaName !== propName) { debugProperty(`Java name ${javaName} is different from ${propName}`); return true; } if (prop.type() === 'object') { debugProperty('Recursing into object'); const check = checkPropertyNames(propName, prop); if (check) { return true; } } else if (prop.type() === 'array') { debugProperty(`checkPropertyNames: ${prop}`); if (!prop.items) { throw new Error(`Array named ${ propName } must have an 'items' property to indicate what type the array elements are.`); } const itemsType = prop.items().type(); debugProperty(`checkPropertyNames: ${prop.items}`); debugProperty(`array of ${itemsType}`); if (itemsType === 'object') { debugProperty('Recursing into array'); const check = checkPropertyNames(propName, prop.items()); if (check) { return true; } } } } return ret; } function dump(obj) { let s = typeof obj; for (const p in obj) { s += ' '; s += p; } return s; } function getAdditionalSubs(asyncapi, params) { let ret; const funcs = getFunctionSpecs(asyncapi, params); funcs.forEach((spec, name, map) => { debugProperty(`getAdditionalSubs: ${spec.name} ${spec.isQueueWithSubscription} ${spec.additionalSubscriptions}`); // The first additional subscription will be the destination. If there is more than one the rest go here. if (spec.isQueueWithSubscription && spec.additionalSubscriptions.length > 1) { if (!ret) { ret = {}; ret.bindings = {}; } const bindingName = `${spec.subscribeBindingName}`; ret.bindings[bindingName] = {}; ret.bindings[bindingName].consumer = {}; ret.bindings[bindingName].consumer.queueAdditionalSubscriptions = spec.additionalSubscriptions.slice(1); } }); return ret; } function getBrokerSettings(asyncapi, params) { let brokerSettings; if (params.useServers === 'true') { let brokers = ''; for (const serverName in asyncapi.servers()) { const server = asyncapi.servers()[serverName]; let url = server.url(); if (server.variables()) { url = generatorFilters.replaceServerVariablesWithValues(url, server.variables()); } brokers += `${url},`; } brokers = brokers.substring(0, brokers.length - 1); brokerSettings = {}; brokerSettings.binder = {}; brokerSettings.binder.brokers = brokers; } return brokerSettings; } // This returns the SCSt bindings config that will appear in application.yaml. function getBindings(asyncapi, params) { const ret = {}; const funcs = getFunctionSpecs(asyncapi, params); const functionsThatAreBeans = filterOutNonBeanFunctionSpecs(funcs); functionsThatAreBeans.forEach((spec, name, map) => { if (spec.isPublisher) { ret[spec.publishBindingName] = {}; ret[spec.publishBindingName].destination = spec.publishChannel; } if (spec.isSubscriber) { ret[spec.subscribeBindingName] = {}; ret[spec.subscribeBindingName].destination = decodeURI(spec.subscribeChannel); if (spec.group) { ret[spec.subscribeBindingName].group = spec.group; } } }); return ret; } // This returns the base function name that SCSt will use to map functions with bindings. function getFunctionName(channelName, operation, isSubscriber) { if (operation.ext('x-scs-function-name')) { return operation.ext('x-scs-function-name'); } let ret; let functionName; const smfBinding = operation.binding('smf'); if (smfBinding && smfBinding.queueName && smfBinding.topicSubscriptions) { functionName = smfBinding.queueName; } else { if (operation.id()) { return operation.id(); } functionName = channelName; } if (isSubscriber === undefined) { ret = _.camelCase(functionName); } else { ret = _.camelCase(functionName) + (isSubscriber ? 'Consumer' : 'Supplier'); } return ret; } function filterOutNonBeanFunctionSpecs(funcs) { const beanFunctionSpecs = new Map(); const entriesIterator = funcs.entries(); let iterValue = entriesIterator.next(); while (!iterValue.done) { const funcSpec = iterValue.value[1]; // This is the inverse of the condition in the Application template file // Add it if its not dynamic (hasParams = true, isPublisher = true) and type is supplier or streamBridge if (!(funcSpec.isPublisher && funcSpec.channelInfo.hasParams && (funcSpec.type === 'supplier' || funcSpec.dynamicType === 'streamBridge'))) { beanFunctionSpecs.set(iterValue.value[0], iterValue.value[1]); } iterValue = entriesIterator.next(); } return beanFunctionSpecs; } // This returns the string that gets rendered in the function.definition part of application.yaml. function getFunctionDefinitions(asyncapi, params) { let ret = ''; const funcs = getFunctionSpecs(asyncapi, params); const functionsThatAreBeans = filterOutNonBeanFunctionSpecs(funcs); const names = functionsThatAreBeans.keys(); ret = Array.from(names).join(';'); return ret; } function getFunctionSpecs(asyncapi, params) { // This maps function names to SCS function definitions. debugFunction('--------------------------- getFunctionSpecs -------------------'); if (params.dynamicType !== 'header' && params.dynamicType !== 'streamBridge') { throw new Error(`The dynamicType parameter must be either 'header' or 'streamBridge. Given: ${params.dynamicType}`); } const functionMap = new Map(); const reactive = params.reactive === 'true'; const info = asyncapi.info(); for (const channelName in asyncapi.channels()) { const channel = asyncapi.channels()[channelName]; debugFunction(`getFunctionSpecs channelName ${channelName}`); debugFunction(channel); let functionSpec; const publish = scsLib.getRealPublisher(info, params, channel); if (publish) { const name = getFunctionName(channelName, publish, false); debugFunction(`We have a real publisher named ${name}`); functionSpec = functionMap.get(name); if (functionSpec) { if (functionSpec.isPublisher) { throw new Error(`Function ${name} can't publish to both channels ${name} and ${channelName}.`); } functionSpec.type = 'function'; debugFunction('Found existing subscriber, so this is a function.'); } else { const channelInfo = getChannelInfo(params, channelName, channel); functionSpec = new SCSFunction(); functionSpec.name = name; functionSpec.type = 'supplier'; functionSpec.reactive = reactive; functionSpec.dynamic = channelInfo.hasParams; functionSpec.channelInfo = channelInfo; functionSpec.sendMethodName = getSendFunctionName(channelName, publish); functionSpec.dynamicType = params.dynamicType; functionSpec.parametersToHeaders = params.parametersToHeaders; functionSpec.multipleMessageComment = getMultipleMessageComment(publish); functionMap.set(name, functionSpec); } const payload = getPayloadClass(publish); if (!payload) { throw new Error(`Channel ${channelName}: no payload class has been defined.`); } functionSpec.publishPayload = payload; functionSpec.publishChannel = channelName; } const subscribe = scsLib.getRealSubscriber(info, params, channel); if (subscribe) { const name = getFunctionName(channelName, subscribe, true); debugFunction(`We have a real subscriber named ${name}`); const smfBinding = subscribe.binding('smf'); debugFunction('smfBinding:'); debugFunction(smfBinding); functionSpec = functionMap.get(name); if (functionSpec) { debugFunction(`This already exists: ${name} isQueueWithSubscription: ${functionSpec.isQueueWithSubscription}`); if (functionSpec.isQueueWithSubscription) { // This comes from an smf binding to a queue. debugFunction(functionSpec); for (const sub of smfBinding.channelSubscriptions) { let foundIt = false; for (const existingSub of functionSpec.additionalSubscriptions) { debugFunction(`Comparing ${sub} to ${existingSub}`); if (sub === existingSub) { foundIt = true; break; } } if (!foundIt) { debugFunction(`Adding new sub ${sub}`); functionSpec.additionalSubscriptions.push(sub); functionSpec.multipleMessages = true; } } debugFunction(`The queue ${functionSpec.name} has multiple subs: ${functionSpec.multipleMessages}`); if (functionSpec.multipleMessages) { functionSpec.subscribePayload = 'Message<?>'; } debugFunction('Updated function spec:'); debugFunction(functionSpec); continue; } else { if (functionSpec.isSubscriber) { throw new Error(`Function ${name} can't subscribe to both channels ${functionSpec.channel} and ${channelName}.`); } functionSpec.type = 'function'; functionSpec.dynamicType = params.dynamicType; } } else { debugFunction('This is a new one.'); const channelInfo = getChannelInfo(params, channelName, channel); functionSpec = new SCSFunction(); functionSpec.name = name; functionSpec.type = 'consumer'; functionSpec.reactive = reactive; functionSpec.dynamic = channelInfo.hasParams; functionSpec.channelInfo = channelInfo; functionSpec.dynamicType = params.dynamicType; functionSpec.parametersToHeaders = params.parametersToHeaders; functionSpec.multipleMessageComment = getMultipleMessageComment(subscribe); if (smfBinding && smfBinding.queueName && smfBinding.topicSubscriptions) { debugFunction(`A new one with subscriptions: ${smfBinding.topicSubscriptions}`); functionSpec.additionalSubscriptions = smfBinding.topicSubscriptions; functionSpec.isQueueWithSubscription = true; functionSpec.multipleMessages = smfBinding.topicSubscriptions && smfBinding.topicSubscriptions.length > 1; } functionMap.set(name, functionSpec); } if (functionSpec.multipleMessages) { functionSpec.subscribePayload = 'Message<?>'; } else { const payload = getPayloadClass(subscribe); if (!payload) { throw new Error(`Channel ${channelName}: no payload class has been defined.`); } functionSpec.subscribePayload = payload; } const group = subscribe.ext('x-scs-group'); if (group) { functionSpec.group = group; } const dest = subscribe.ext('x-scs-destination'); if (dest) { functionSpec.subscribeChannel = dest; } else if (functionSpec.isQueueWithSubscription) { functionSpec.subscribeChannel = functionSpec.additionalSubscriptions[0]; debugFunction(`Setting subscribeChannel for channelWithSubs: ${functionSpec.subscribeChannel}`); } else { const channelInfo = getChannelInfo(params, channelName, channel); functionSpec.subscribeChannel = channelInfo.subscribeChannel; } } debugFunction('functionSpec:'); debugFunction(functionSpec); } return functionMap; } function getSendFunctionName(channelName, operation) { return `send${_.upperFirst(getFunctionName(channelName, operation, undefined))}`; } function getMultipleMessageComment(pubOrSub) { let ret; // We deliberately leave out the last newline, because that makes it easier to use in the template. // Otherwise it's really hard to get rid of an extra unwanted newline. if (pubOrSub.hasMultipleMessages()) { ret = '// The message can be of type:'; pubOrSub.messages().forEach(m => { ret += '\n\t// '; ret += getMessagePayloadType(m); }); } return ret; } function getPayloadClass(pubOrSub) { let ret; debugPayload(pubOrSub); if (pubOrSub.hasMultipleMessages()) { ret = 'Message<?>'; } else { const message = pubOrSub.message(); if (message) { ret = getMessagePayloadType(message); } } debugPayload(`getPayloadClass: ${ret}`); return ret; } function getMessagePayloadType(message) { let ret; const payload = message.payload(); debugPayload('payload:'); debugPayload(payload); if (payload) { const type = payload.type(); debugPayload('type:'); debugPayload(type); if (!type || type === 'object') { ret = payload.ext('x-parser-schema-id'); const { className } = scsLib.stripPackageName(ret); ret = _.camelCase(className); ret = _.upperFirst(ret); } else { ret = getType(type, payload.format()).javaType; } } return ret; } function sortParametersUsingChannelName(parameters, channelName) { // This doesnt work if theres two of the same variables in the channel name (scenario unlikely) parameters.forEach(param => { param.indexInChannelName = channelName.indexOf(param.rawName); }); return _.sortBy(parameters, ['indexInChannelName']); } // This returns the connection properties for a solace binder, for application.yaml. function getSolace(params) { const ret = {}; ret.java = {}; ret.java.host = params.host; ret.java.msgVpn = params.msgVpn; ret.java.clientUsername = params.username; ret.java.clientPassword = params.password; return ret; } // This returns an object containing information the template needs to render channel strings. function getChannelInfo(params, channelName, channel) { const ret = {}; // This isfor the parameterToHeader feature. const delimiter = (params.binder === 'rabbit' || params.binder === 'kafka') ? '.' : '/'; const channelParts = channelName.split(delimiter); let publishChannel = String(channelName); let subscribeChannel = String(channelName); const parameters = []; debugChannel('parameters:'); debugChannel(channel.parameters()); for (const name in channel.parameters()) { const nameWithBrackets = `{${name}}`; const parameter = channel.parameter(name); const schema = parameter.schema(); const type = getType(schema.type(), schema.format()); const param = { name: _.camelCase(name), rawName: name }; debugChannel(`name: ${name} type:`); debugChannel(type); let sampleArg = 1; subscribeChannel = subscribeChannel.replace(nameWithBrackets, '*'); // Figure out what channel part it's in. This is just for the parameterToHeader feature. for (let i = 0; i < channelParts.length; i++) { if (channelParts[i] === nameWithBrackets) { param.position = i; break; } } [publishChannel, sampleArg] = handleParameterType(name, param, type, publishChannel, schema, nameWithBrackets); param.sampleArg = sampleArg; parameters.push(param); } // The channel parameters aren't in any particular order when they come in. // This means, to be safe, we need to order them like how it is in the channel name. ret.parameters = sortParametersUsingChannelName(parameters, channelName); ret.functionArgList = ret.parameters.map(param => param.name).join(', '); ret.functionParamList = ret.parameters.map(param => `${param.type} ${param.name}`).join(', '); ret.sampleArgList = ret.parameters.map(param => param.sampleArg).join(', '); ret.channelName = channelName; ret.publishChannel = publishChannel; ret.subscribeChannel = subscribeChannel; ret.hasParams = parameters.length > 0; return ret; } function handleParameterType(name, param, type, publishChannel, schema, nameWithBrackets) { let sampleArg = 1; if (type) { debugChannel('It is a type:'); debugChannel(type); const javaType = type.javaType || typeMap.get(type); if (!javaType) throw new Error(`channelInfo filter: type not found in typeMap: ${type}`); param.type = javaType; const printfArg = type.printFormat; debugChannel(`printf: ${printfArg}`); if (!printfArg) throw new Error(`channelInfo filter: printFormat not found in formatMap: ${type}`); debugChannel(`Replacing ${nameWithBrackets}`); publishChannel = publishChannel.replace(nameWithBrackets, printfArg); sampleArg = type.sample; } else { const en = schema.enum(); if (en) { debugChannel(`It is an enum: ${en}`); param.type = _.upperFirst(name); param.enum = en; sampleArg = `Messaging.${param.type}.${en[0]}`; debugChannel(`Replacing ${nameWithBrackets}`); publishChannel = publishChannel.replace(nameWithBrackets, '%s'); } else { throw new Error(`channelInfo filter: Unknown parameter type: ${ JSON.stringify(schema)}`); } } param.sampleArg = sampleArg; return [publishChannel, sampleArg]; } function indent(numTabs) { return '\t'.repeat(numTabs); } function isApplication(params) { return params.artifactType === 'application'; }