Skip to content

Commit ace6773

Browse files
Merge pull request diffblue#510 from diffblue/feature/di-bean-annotations
DI bean annotations [SEC-545]
2 parents 4348968 + f2f470f commit ace6773

File tree

4 files changed

+411
-59
lines changed

4 files changed

+411
-59
lines changed

env-model-generator/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"build": "tsc",
1717
"lint": "tslint src/**/*.ts",
1818
"fix-lint": "tslint src/**/*.ts --fix",
19+
"gen-docs": "typedoc --out doc",
1920
"start": "node built/env-model-generator.js ../benchmarks/GENUINE/sakai/edu-services/cm-service/cm-impl/hibernate-impl/impl/src/test/spring-test.xml --outputPath output"
2021
},
2122
"dependencies": {
@@ -27,11 +28,12 @@
2728
},
2829
"devDependencies": {
2930
"@types/fs-extra": "5.0.0",
30-
"@types/lodash": "4.14.68",
31+
"@types/lodash": "^4.14.115",
3132
"@types/node": "8.0.47",
3233
"@types/xml2js": "0.4.2",
3334
"tslint": "5.4.2",
3435
"tslint-language-service": "0.9.6",
36+
"typedoc": "^0.11.1",
3537
"typescript": "2.4.1"
3638
}
3739
}

env-model-generator/src/parseJson.ts

+137-28
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,38 @@ import * as fs from "fs-extra";
22
import * as jsonConfig from "./annotationConfig";
33
import { ConfigParseError } from "./configParseError";
44
import * as model from "./model";
5+
import { getBeanById } from "./parserDriver";
56
import { lowerInitialChar } from "./utils";
67

78
interface NamedComponent {
89
qualifiedClassName: string;
910
component: jsonConfig.Component;
1011
}
1112

12-
const beansById = new Map<string, NamedComponent>();
13+
/**
14+
* [[BeanDefinitionMethod]] does not include the name of the method or the name of the class it is defined in; these are provided by the context.
15+
*
16+
* This class therefore combines these bits of information to act as the value to be used in a map from calculated identifiers to bean definition
17+
* methods.
18+
*/
19+
interface NamedBeanDefinitionMethod {
20+
className: string;
21+
name: string;
22+
method: jsonConfig.BeanDefinitionMethod;
23+
}
24+
25+
// A map from identifiers to components discovered in the JSON file.
26+
const componentsById = new Map<string, NamedComponent>();
27+
// A map from identifiers to bean definition methods discovered in the JSON file.
28+
const beansById = new Map<string, NamedBeanDefinitionMethod>();
1329

30+
/**
31+
* Collect beans from the JSON configuration file without parsing them.
32+
*
33+
* This allows us to set up objects that can be used to resolve circular references discovered later during parsing.
34+
* @param filePath The path to the configuration file to read the input from
35+
* @returns Paths to other config files to also include
36+
*/
1437
export async function collectBeansFromConfigFile(filePath: string): Promise<string[]> {
1538
let json: string;
1639
try {
@@ -22,59 +45,145 @@ export async function collectBeansFromConfigFile(filePath: string): Promise<stri
2245
for (const qualifiedClassName in annotationConfig.components) {
2346
if (!annotationConfig.components.hasOwnProperty(qualifiedClassName))
2447
continue;
25-
// Create bean ID
26-
// TODO: Handle name parameter to @Component constructor
48+
// TODO: Handle name value on @Component annotation
2749
const id = lowerInitialChar(<string>qualifiedClassName.split(".").pop());
28-
// Check for duplicate ids from other beans
29-
if (beansById.has(id))
30-
throw new ConfigParseError(`Found multiple components with id '${id}'`);
31-
const aliasedBeanId = model.Bean.getAlias(id);
32-
if (aliasedBeanId !== undefined)
33-
throw new ConfigParseError(`Found a bean with id same as an alias for '${aliasedBeanId}'`);
34-
// Create a bean model for the bean
35-
const component = { qualifiedClassName: qualifiedClassName, component: annotationConfig.components[qualifiedClassName] };
36-
beansById.set(id, component);
37-
model.Bean.registerIdForClass(component.qualifiedClassName, id);
50+
checkBeanId(id, `@Component annotation on class '${qualifiedClassName}'`);
51+
componentsById.set(id, { qualifiedClassName: qualifiedClassName, component: annotationConfig.components[qualifiedClassName] });
52+
model.Bean.registerIdForClass(qualifiedClassName, id);
53+
}
54+
for (const configurationClassName in annotationConfig.configurations) {
55+
if (!annotationConfig.configurations.hasOwnProperty(configurationClassName))
56+
continue;
57+
const beanDefinitionMethods = annotationConfig.configurations[configurationClassName].beanDefinitionMethods;
58+
for (const beanDefinitionMethodName in beanDefinitionMethods) {
59+
if (!beanDefinitionMethods.hasOwnProperty(beanDefinitionMethodName))
60+
continue;
61+
const beanDefinitionMethod = beanDefinitionMethods[beanDefinitionMethodName];
62+
let id = beanDefinitionMethodName;
63+
if (beanDefinitionMethod.specifiedNames !== undefined && beanDefinitionMethod.specifiedNames.length !== 0) {
64+
id = beanDefinitionMethod.specifiedNames[0];
65+
// Add aliases
66+
for (let i = 1; i < beanDefinitionMethod.specifiedNames.length; ++i)
67+
model.Bean.addAlias(beanDefinitionMethod.specifiedNames[i], id);
68+
}
69+
checkBeanId(id, `@Bean annotation on '${configurationClassName}.${beanDefinitionMethodName}'`);
70+
beansById.set(id, { className: configurationClassName, name: configurationClassName, method: beanDefinitionMethod });
71+
model.Bean.registerIdForClass(beanDefinitionMethod.type, id);
72+
}
3873
}
3974
// TODO: Return paths to XML config files specified in code
4075
return [];
4176
}
4277

78+
/**
79+
* Check for duplicate ids from other beans or aliases
80+
* @param id The bean identifier to check
81+
* @param location The location to include in the text of any thrown exception
82+
* @throws [[ConfigParseError]] If the bean identifier has already been used
83+
*/
84+
function checkBeanId(id: string, location: string): void {
85+
if (componentsById.has(id))
86+
throw new ConfigParseError(`${location}: There is an existing component with id '${id}'`);
87+
if (beansById.has(id))
88+
throw new ConfigParseError(`${location}: There is an existing bean with id '${id}'`);
89+
const aliasedBeanId = model.Bean.getAlias(id);
90+
if (aliasedBeanId !== undefined)
91+
throw new ConfigParseError(`${location}: There is an existing alias with identifier '${aliasedBeanId}'`);
92+
}
93+
94+
/**
95+
* Check whether a bean identifier is already implemented by an element from the JSON configuration
96+
* @param id The bean identifier to check
97+
* @returns True if the bean identifier is already registered
98+
*/
4399
export function beanWithIdExists(id: string): boolean {
44-
return beansById.has(id);
100+
return componentsById.has(id) || beansById.has(id);
45101
}
46102

103+
/**
104+
* Create the model for each bean discovered during the execution of [[collectBeansFromConfigFile]]
105+
*/
47106
export function parseAllBeans(): void {
48-
for (const [ beanId, bean ] of beansById) {
107+
for (const [ beanId, component ] of componentsById) {
108+
if (model.Bean.tryGet(beanId) !== undefined)
109+
continue;
110+
// We don't store the results of this parsing, it is stored in internal data structures which do enough for us
111+
parseJsonComponent(beanId, component);
112+
}
113+
for (const [ beanId, namedMethod ] of beansById) {
49114
if (model.Bean.tryGet(beanId) !== undefined)
50115
continue;
51116
// We don't store the results of this parsing, it is stored in internal data structures which do enough for us
52-
parseJsonBean(beanId, bean);
117+
parseJsonBean(beanId, namedMethod);
53118
}
54119
}
55120

121+
/**
122+
* Create the model for a bean with the given identifier
123+
* @param id The identifier of a bean implemented by a component or bean definition method
124+
* @returns The model for the bean
125+
*/
56126
export function parseBean(id: string): model.Bean {
127+
const component = componentsById.get(id);
128+
if (component !== undefined)
129+
return parseJsonComponent(id, component);
57130
const bean = beansById.get(id);
58-
if (bean === undefined)
59-
throw new Error("Called parseBean on a bean that did not exist");
60-
return parseJsonBean(id, bean);
131+
if (bean !== undefined)
132+
return parseJsonBean(id, bean);
133+
throw new Error("Called parseBean on a bean that did not exist");
61134
}
62135

63-
function parseJsonBean(id: string | undefined, namedBean: NamedComponent): model.Bean {
136+
/**
137+
* Create the model for a bean with the given identifier defined by a component from the JSON configuration file
138+
* @param id The identifier of a bean implemented by a component
139+
* @param namedBean The component definition from the JSON configuration file
140+
* @returns The model for the bean
141+
*/
142+
function parseJsonComponent(id: string, namedBean: NamedComponent): model.Bean {
64143
const component = namedBean.component;
65144
const properties: model.BeanProperty[] = [];
66145
for (const fieldName in component.fields) {
67146
if (!component.fields.hasOwnProperty(fieldName))
68147
continue;
69-
const fieldClass = component.fields[fieldName].type;
70-
71-
const fieldBeanId = model.Bean.tryGetIdByClass(fieldClass);
72-
if (fieldBeanId === undefined)
73-
throw new ConfigParseError(
74-
`Auto-wired field '${namedBean.qualifiedClassName}.${fieldName}' depends on a class '${fieldClass}' `
75-
+ `that has ${model.Bean.hasMultipleBeansForClass(fieldClass) ? "multiple" : "no"} implementations`);
76-
properties.push({ name: fieldName, value: new model.BeanRefValue(fieldBeanId) });
148+
properties.push({
149+
name: fieldName,
150+
value: getBeanForClass(component.fields[fieldName].type, `Auto-wired field '${namedBean.qualifiedClassName}.${fieldName}'`),
151+
});
77152
}
78153
// TODO: Handle constructor arguments
79154
return new model.Bean(undefined, id, namedBean.qualifiedClassName, false, [], properties);
80155
}
156+
157+
/**
158+
* Create the model for a bean with the given identifier defined by a component from the JSON configuration file
159+
* @param id The identifier of a bean implemented by a bean definition method
160+
* @param namedMethod The bean definition method definition from the JSON configuration file
161+
* @returns The model for the bean
162+
*/
163+
function parseJsonBean(id: string, namedMethod: NamedBeanDefinitionMethod): model.Bean {
164+
const fieldBeanId = model.Bean.tryGetIdByClass(namedMethod.className);
165+
if (fieldBeanId === undefined)
166+
throw new ConfigParseError(
167+
`Factory method '${namedMethod.name}' is defined in a class '${namedMethod.className}' `
168+
+ `that has ${model.Bean.hasMultipleBeansForClass(namedMethod.className) ? "multiple" : "no"} implementations`);
169+
const factoryBean = getBeanById(fieldBeanId);
170+
if (factoryBean === undefined)
171+
throw new ConfigParseError(
172+
`Couldn't find bean '${namedMethod.className}' that is configuration containing factory method '${namedMethod.name}'`);
173+
const method = namedMethod.method;
174+
const argumentValues: model.Value[] = [];
175+
for (const parameterType of method.parameterTypes) {
176+
argumentValues.push(getBeanForClass(parameterType, `Parameter to factory method '${namedMethod.className}.${namedMethod.name}'`));
177+
}
178+
const isLazyInit = method.scope !== undefined && method.scope !== "singleton" || method.isLazyInit === true;
179+
return new model.Bean(undefined, id, { bean: factoryBean, method: namedMethod.name }, isLazyInit, argumentValues, []);
180+
}
181+
182+
function getBeanForClass(className: string, userString: string): model.BeanRefValue {
183+
const fieldBeanId = model.Bean.tryGetIdByClass(className);
184+
if (fieldBeanId === undefined)
185+
throw new ConfigParseError(
186+
`${userString} depends on a class '${className}' `
187+
+ `that has ${model.Bean.hasMultipleBeansForClass(className) ? "multiple" : "no"} implementations`);
188+
return new model.BeanRefValue(fieldBeanId);
189+
}

env-model-generator/src/parseSpringXml.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ function parseXmlBean(id: string | undefined, bean: spring.BeanType): model.Bean
106106
throw new ConfigParseError(
107107
`Couldn't find bean '${bean.factoryBean}' specified as factoryBean of ${id === undefined ? "anonymous bean" : `'${id}'`}`);
108108
}
109-
const isLazyInit = bean.scope !== undefined && bean.scope !== "prototype" || bean.lazyInit === "true";
109+
const isLazyInit = bean.scope !== undefined && bean.scope !== "singleton" || bean.lazyInit === "true";
110110
// For each constructor argument parse the index and create a map from index to the whole argument object
111111
const constructorArgMap =
112112
new Map((bean.constructorArg || []).map((arg) => [ parseInt(arg.index, 10), arg ] as [ number, spring.ConstructorArgType ]));

0 commit comments

Comments
 (0)