I am using Swagger version 2 with Java Spring. I have declared a property and it works fine and it generates a drop down list of value I assigned.
#ApiParam(value = "Pass any one Shuttle provider ID from the list", allowableValues = "1,2,3,4,10")
private Long hotelId;
Now, I need a way to populate this list which is passed in allowableValues from my database as it could be random list as well as huge data. How can I assign list of values dynamically from database in this allowableValues?
This question is bit old, I too faced the same problem so thought of adding here which may help some one.
//For ApiModelProperty
#ApiModelProperty(required = true, allowableValues = "dynamicEnum(AddressType)")
#JsonProperty("type")
private String type;
Created a component which implements ModelPropertyBuilderPlugin
#Component
#Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1)
public class ApiModelPropertyPropertyBuilderCustom implements ModelPropertyBuilderPlugin {
private final DescriptionResolver descriptions;
#Autowired
public ApiModelPropertyPropertyBuilderCustom(DescriptionResolver descriptions) {
this.descriptions = descriptions;
}
public void apply(ModelPropertyContext context) {
try {
AllowableListValues allowableListValues = (AllowableListValues) FieldUtils.readField(context.getBuilder(),
"allowableValues", true);
if(allowableListValues!=null) {
String allowableValuesString = allowableListValues.getValues().get(0);
if (allowableValuesString.contains("dynamicEnum")) {
String yourOwnStringOrDatabaseTable = allowableValuesString.substring(allowableValuesString.indexOf("(")+1, allowableValuesString.indexOf(")"));
//Logic to Generate dynamic values and create a list out of it and then create AllowableListValues object
context.getBuilder().allowableValues(allowableValues);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean supports(DocumentationType delimiter) {
return SwaggerPluginSupport.pluginDoesApply(delimiter);
}
}
Similary for ApiParam we can create component which will implement ParameterBuilderPlugin
#Override
public void apply(ParameterContext context) {
#SuppressWarnings("Guava") final Optional<ApiParam> apiParam =
context.resolvedMethodParameter().findAnnotation(ApiParam.class);
if (apiParam.isPresent()) {
final String allowableValuesString = apiParam.get().allowableValues();
//Your logic here
context.parameterBuilder().allowableValues(allowableValues);
}
}
You need to create constructor in SwaggerConfiguration class.
#Autowire service and withdraw data you need from database
assign this to final variable
assign this final variable to allowableValues in annotation
enjoy not efficient api
private final String allowableValues;
public SwaggerConfiguration() {
List<YourEntitiy> list = someService.findAll();
//code to get every value you need and add create comma separated String
StringJoiner stringJoiner = new StringJoiner(",");
stringJoiner.add(list.get(0).getValue());
this.allowableValues = stringJoiner.toString();
}
#ApiParam(allowableValues = allowableValues)
But I think it's bad idea getting all ids from database just to create allowable values. Just validate in api method if that id exist and/or Create new api to get ids from database, use pagination from Spring Data project, like PageImpl<> javadocs
Related
I'm thinking at the moment about the solution of a problem which might be already solved by a pattern but I can not find the correct one. I'll try to explain what I would like to do with a simplefied example.
There is a class which handles to connection to a database, let's call it DatabaseManager.java. With this class I would like to handle the fetchment of data from a database and I also would like to apply filter.
public class DatabaseManager {
DatabaseFilter databaseFilter = new DatabaseFilter();
public DatabaseManager() {
// Do some stuff to init db connection
}
public DatabaseFilter configureFilter() {
return databaseFilter;
}
public String getStringDataset() {
String dataset = null;
// Fill dataset with applied filter data
return dataset;
}
}
With the method call configureFilter() on the databse object, I would like to get the DatabaseFilter which contains particular nested filter classes.
public class DatabaseFilter {
int[] filteredIds = null;
public IdFilter onId() {
return new IdFilter();
}
public class IdFilter {
private void exclude(int[] ids) {
filteredIds = ids;
}
}
}
On this way, I could write the follwing nice syntax to configure a filter which excludes particular filter.
DatabaseManager database = new DatabaseManager();
database.configureFilter().onId().exclude(idArray);
// Get filtered dataset
database.getStringDataset();
Is this an appropriate way to solve problems like this and is this a particular pattern? Are there any disadvantages?
I have a builder class which I am using in one of my project.
Let's say I have metricA as builder based on below class.
I need to make a new builder metricB based on metricA by cloning metricA so that metricB contains all the values which were already there in metricA.
In the constructor of MetricHolder I am initializing some fields (which are not set directly) basis on fields that have been set already.
clientTypeOrPayId - I am initializing this field. If payId is present, then I will set this value or I will set clientType.
clientKey - I am initializing this field as well in the same constructor.
And most importantly, I am putting few mandatory fields in the clientPayload map. I am not sure what is the right way to do that. But I need to add is_clientid and is_deviceid into the map. (In general I am adding few more fields).
And then in the last of the constructor, I am calculating latency difference and sending it to some other system.
Below is my class:
public final class MetricHolder {
private final String clientId;
private final String deviceId;
private final String payId;
private final String clientType;
private final String clientTypeOrPayId;
private final Schema schema;
private final String schemaId;
private final String clientKey;
private final Map<String, String> clientPayload;
private final Record record;
private final long clientCreateTimestamp;
private final long clientSentTimestamp;
private MetricHolder(Builder builder) {
this.payId = builder.payId;
this.siteId = builder.siteId;
this.clientType = builder.clientType;
this.clientId = builder.clientId;
this.deviceId = builder.deviceId;
this.schema = builder.schema;
this.schemaId = builder.schemaId;
// populating all the required fields in the map and make it immutable
// not sure whether this is right?
builder.clientPayload.put("is_clientid", (clientId == null) ? "false" : "true");
builder.clientPayload.put("is_deviceid", (clientId == null) ? "true" : "false");
this.clientPayload = Collections.unmodifiableMap(builder.clientPayload);
this.clientTypeOrPayId = Strings.isNullOrEmpty(payId) ? clientType : payId;
this.record = builder.record;
this.clientKey = "process:" + System.currentTimeMillis() + ":"
+ ((clientId == null) ? deviceId : clientId);
this.clientCreateTimestamp = builder.clientCreateTimestamp;
this.clientSentTimestamp = builder.clientSentTimestamp;
// this will be called twice while cloning
// what is the right way to do this then?
SendData.getInstance().insert(clientTypeOrPayId,
System.currentTimeMillis() - clientCreateTimestamp);
SendData.getInstance().insert(clientTypeOrPayId,
System.currentTimeMillis() - clientSentTimestamp);
}
public static class Builder {
private final Record record;
private Schema schema;
private String schemaId;
private String clientId;
private String deviceId;
private String payId;
private String clientType;
private Map<String, String> clientPayload;
private long clientCreateTimestamp;
private long clientSentTimestamp;
// this is for cloning
public Builder(MetricHolder packet) {
this.record = packet.record;
this.schema = packet.schema;
this.schemaId = packet.schemaId;
this.clientId = packet.clientId;
this.deviceId = packet.deviceId;
this.payId = packet.payId;
this.clientType = packet.clientType;
// make a new map and check whether mandatory fields are present already or not
// and if they are present don't add it again.
this.clientPayload = new HashMap<>();
for (Map.Entry<String, String> entry : packet.clientPayload.entrySet()) {
if (!("is_clientid".equals(entry.getKey()) || "is_deviceid".equals(entry.getKey())) {
this.clientPayload.put(entry.getKey(), entry.getValue());
}
}
this.clientCreateTimestamp = packet.clientCreateTimestamp;
this.clientSentTimestamp = packet.clientSentTimestamp;
}
public Builder(Record record) {
this.record = record;
}
public Builder setSchema(Schema schema) {
this.schema = schema;
return this;
}
public Builder setSchemaId(String schemaId) {
this.schemaId = schemaId;
return this;
}
public Builder setClientId(String clientId) {
this.clientId = clientId;
return this;
}
public Builder setDeviceId(String deviceId) {
this.deviceId = deviceId;
return this;
}
public Builder setPayId(String payId) {
this.payId = payId;
return this;
}
public Builder setClientType(String clientType) {
this.clientType = clientType;
return this;
}
public Builder setClientPayload(Map<String, String> payload) {
this.clientPayload = payload;
return this;
}
public Builder setClientCreateTimestamp(long clientCreateTimestamp) {
this.clientCreateTimestamp = clientCreateTimestamp;
return this;
}
public Builder setClientSentTimestamp(long clientSentTimestamp) {
this.clientSentTimestamp = clientSentTimestamp;
return this;
}
public MetricHolder build() {
return new MetricHolder(this);
}
}
// getters
}
Question:-
Below is how I make metricA builder object:
MetricHolder metricA = new MetricHolder.Builder(record).setClientId("123456").setDeviceId("abcdefhg")
. setPayId("98765").setClientPayload(payloadMapHolder).setClientCreateTimestamp(createTimestamp)
.setClientSentTimestamp(sentTimestamp).build();
Now this is how I clone the metricA object later on in the code when I get all other fields as shown below:
MetricHolder metricB = new MetricHolder.Builder(metricA).setSchema(schema).setSchemaId("345").build();
I see two problem now:
First of all, my SendData.getInstance() line in the MetricHolder constructor will be called twice. First is when I make metricA and second when I make metricB by cloning metricA. But I just want to call it only once when I try to create metricA builder object? How can I make this possible?
Second is, the way I am populating clientPayload map with two mandatory fields in the MetricHolder constructor doesn't look right to me. Is there any other better way to do the same thing?
I guess the whole problem is happening because the way I am cloning metricA to make a metricB builder object? What is the best way to do this? I want to achieve above two things but in a right way.
But I just want to call it only once when I try to create metricA builder object? How can I make this possible?
The most straightforward way is to have a flag in the builder indicating whether it was created by Record or by cloning:
class Builder {
final boolean cloned;
Builder(MetricHolder packet) {
this.cloned = true;
// ...
}
Builder(Record record) {
this.cloned = false;
// ...
}
}
Then, in the constructor of MetricHolder:
if (!builder.cloned) {
SendData.getInstance().whatever();
}
But it's worth pointing out that making this call to SendData is an example of doing too much work in the constructor. You should think carefully about whether you really want to be making this call in the constructor, or whether you can factor that out into another method.
Second is, the way I am populating clientPayload map with two mandatory fields in the MetricHolder constructor doesn't look right to me. Is there any other better way to do the same thing?
You've misunderstood the "unmodifiable" bit of using Collections.unmodifiableMap: it's only an unmodifiable view of the map parameter; you can still modify the underlying map.
Here's a JUnit test to demonstrate:
Map<String, String> original = new HashMap<>();
original.put("hello", "world");
// Obviously false, we just put something into it.
assertFalse(original.isEmpty());
Map<String, String> unmodifiable = Collections.unmodifiableMap(original);
// We didn't modify the original, so we don't expect this to have changed.
assertFalse(original.isEmpty());
// We expect this to be the same as for the original.
assertFalse(unmodifiable.isEmpty());
try {
unmodifiable.clear();
fail("Expected this to fail, as it's unmodifiable");
} catch (UnsupportedOperationException expected) {}
// Yep, still the same contents.
assertFalse(original.isEmpty());
assertFalse(unmodifiable.isEmpty());
// But here's where it gets sticky - no exception is thrown.
original.clear();
// Yep, we expect this...
assertTrue(original.isEmpty());
// But - uh-oh - the unmodifiable map has changed!
assertTrue(unmodifiable.isEmpty());
The thing is that the map is only unmodifiable if there is no other reference to it hanging around: if you don't have a reference to original, unmodifiable actually is unmodifiable; otherwise, you can't rely upon the map never changing.
In your particular case, you are simply wrapping the clientPayload map in your unmodifiable collection. So, you're overwrite values for previously-constructed instances.
For example:
MetricHolder.Builder builder = new MetricHolder.Builder();
MetricHolder first = builder.build();
assertEquals("false", first.clientPayload.get("is_clientid"));
assertEquals("true", first.clientPayload.get("is_deviceid"));
builder.setClientId("").build();
// Hmm, first has changed.
assertEquals("true", first.clientPayload.get("is_clientid"));
assertEquals("false", first.clientPayload.get("is_deviceid"));
The correct approach is not to wrap builder.clientPayload. Take a copy of the map, modify it, and then wrap with unmodifiableMap:
{
Map<String, String> copyOfClientPayload = new HashMap<>(builder.clientPayload);
copyOfClientPayload.put("is_clientid", (clientId == null) ? "false" : "true");
copyOfClientPayload.put("is_deviceid", (clientId == null) ? "true" : "false");
this.clientPayload = Collections.unmodifiableMap(copyOfClientPayload);
}
The surrounding {} aren't strictly necessary, but they restrict the scope of copyOfClientPayload, so you can't accidentally reuse it later in the constructor.
I have the following classes:
class ServiceSnapshot {
List<ExchangeSnapshot> exchangeSnapshots = ...
...
}
class ExchangeSnapshot{
Map<String, String> properties = ...
...
}
SayI have a collection of ServiceSnapshots, like so:
Collection<ServiceSnapshot> serviceSnapshots = ...
I'd like to filter the collection so that the resulting collection of ServiceSnapshots only contains ServiceSnapshots that contain ExchangeSnapshots where a property on the ExchangeSnapshots matches a given String.
I have the following untested code, just wondering is there a cleaner/more readable way to do this, using Java 7, and maybe Google Guava if necessary?
Updtae: Note also that the code sample I've provided below isn't suitable for my purposes, since I'm using iterator.remove() to filter the collection. It turns out I cannot do this as it is modifying the underlying collection , meaning subsequent calls to my method below result in fewer and fewer snashots due to previous calls removing them from the collection - this is not what I want.
public Collection<ServiceSnapshot> getServiceSnapshotsForComponent(final String serviceId, final String componentInstanceId) {
final Collection<ServiceSnapshot> serviceSnapshots = getServiceSnapshots(serviceId);
final Iterator<ServiceSnapshot> serviceSnapshotIterator = serviceSnapshots.iterator();
while (serviceSnapshotIterator.hasNext()) {
final ServiceSnapshot serviceSnapshot = (ServiceSnapshot) serviceSnapshotIterator.next();
final Iterator<ExchangeSnapshot> exchangeSnapshotIterator = serviceSnapshot.getExchangeSnapshots().iterator();
while (exchangeSnapshotIterator.hasNext()) {
final ExchangeSnapshot exchangeSnapshot = (ExchangeSnapshot) exchangeSnapshotIterator.next();
final String foundComponentInstanceId = exchangeSnapshot.getProperties().get("ComponentInstanceId");
if (foundComponentInstanceId == null || !foundComponentInstanceId.equals(componentInstanceId)) {
exchangeSnapshotIterator.remove();
}
}
if (serviceSnapshot.getExchangeSnapshots().isEmpty()) {
serviceSnapshotIterator.remove();
}
}
return serviceSnapshots;
}
Using Guava:
Iterables.removeIf(serviceSnapshots, new Predicate<ServiceSnapshot>() {
#Override
public boolean apply(ServiceSnapshot serviceSnapshot) {
return !Iterables.any(serviceSnapshot.getExchangeSnapshots(), new Predicate<ExchangeSnapshot>() {
#Override
public boolean apply(ExchangeSnapshot exchangeSnapshot) {
String foundComponentInstanceId = exchangeSnapshot.getProperties().get("ComponentInstanceId");
return foundComponentInstanceId != null && foundComponentInstanceId.equals(componentInstanceId);
}
});
}
});
I may have a ! missing or inverted somewhere, but the basic strategy is to remove any ServiceSnapshot objects that do not have any ExchangeSnapshot whose ID matches.
Basically, i have a class where i have my arrays in, which is like this
public final class DepotDatabase {
private Driver[] arrayDrivers;
public DepotDatabase() {
arrayDrivers = new Driver[4];
arrayDrivers[0] = new Driver(1234, 1234, 0); // sample driver
arrayDrivers[1] = new Driver(4444, 4444, 0); // sample driver
arrayDrivers[2] = new Driver(1337, 1337, 1); // sample manager
arrayDrivers[3] = new Driver(1234, 1234, 0); // sample driver
}
and i want to print this array in another class, i did set up the array in another class
public Driver(int username, int password, int managerCheck) {
this.username = username;
this.password = password;
this.managerCheck = managerCheck;
}
but now i want to be able to print out all the drivers, but in another class which will be called ViewDrivers or something similar
You can create a method inside DepotDatabase to print the array, then create an object from and call print method.
public final class DepotDatabase {
private Driver[] arrayDrivers;
public void printArray() {
for (int i = 0; i < arrayDrivers.length; i++) {
Driver d = arrayDrivers[i];
System.out.println("Username : " + d.getUsername());
System.out.println("Password : " + d.getPassword());
System.out.println(" Manager Check: " + d.getManagerCheck());
}
}
the from the test class you can do:
public void execute() {
DepotDatabase ddb = new DepotDatabase();
ddb.printArray();
}
That's why you'll need to have getters and setters. You should have:
public Driver[] getDrivers() {
return arrayDrivers;
}
and in the other class, you simply call it (and print it or whatever).
Read this tutorial.
If you plan to print your array in another class you show create an accessor to it.
The common pattern for Java is to use "get plus name off attribute", getDrivers() you should also avoid the class name in such geter as it may changed due to application life.
public final class DepotDatabase {
//your code
public Driver[] getDrivers() {
return this.arrayDrivers;
}
}
Next question to answer is a returning the whole array is good idea. When you return it as above you loose control on it. And every one that call that method will be able to change the content of it.
To prevent this you should use so called Defensive copying
public Driver[] getDrivers() {
return Arrays.copyOf(arrayDrivers, arrayDrivers.length);
}
Then person will get an copy of it an will not harm your class.
The issue with this is that consumer of your class will have to call this method every time to get fresh list of cars.
To solve this issue you may want to user the [collection framework] where instead of array you cold define:
List<Driver> drivers new ArrayList<>();
and provide the drivers as [immutable] list
public Iterable<Driver> getDrivers() {
return java.util.Collections.unmodifiableList(drivers);
}
Iterable is an interface, that allow you to obtain an interator the the list consumer of class wold have possibility to traverse it. IF you wan to allow him to check that list contains some driver you can set the return type as Collection
class Storage {
private String items[] = new String[10];
public String[] getItems() {
return Arrays.copyOf(items, items.length);
}
}
class Store {
Storage storage = new Storage();
private void printStorage() {
String[] items = storage.getItems();
for (String item : items) {
}
}
}
I am trying to invoke a stored procedure which has default (optional) arguments without passing them and it is not working. Essentially the same problem as described here.
My code:
SqlParameterSource in = new MapSqlParameterSource()
.addValue("ownname", "USER")
.addValue("tabname", cachedTableName)
.addValue("estimate_percent", 20)
.addValue("method_opt", "FOR ALL COLUMNS SIZE 1")
.addValue("degree", 0)
.addValue("granularity", "AUTO")
.addValue("cascade", Boolean.TRUE)
.addValue("no_invalidate", Boolean.FALSE)
.addValue("force", Boolean.FALSE);
And I get an exception:
Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Required input parameter 'PARTNAME' is missing
at org.springframework.jdbc.core.CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement(CallableStatementCreatorFactory.java:209)
Where PARTNAME is an optional parameter according to this. Also confirmed by the fact that I can run this procedure w/o the PARTNAME argument manually.
Ater giving up on this question and just passing all the parameters, including optional ones I ran into its inability to pass boolean arguments, because boolean is not an SQL data type, only PL/SQL.
So my current solution is that JDBC is not suited for running stored procedures and this is how I'm working around it:
jdbcTemplate.execute(
new CallableStatementCreator() {
public CallableStatement createCallableStatement(Connection con) throws SQLException{
CallableStatement cs = con.prepareCall("{call sys.dbms_stats.gather_table_stats(ownname=>user, tabname=>'" + cachedMetadataTableName + "', estimate_percent=>20, method_opt=>'FOR ALL COLUMNS SIZE 1', degree=>0, granularity=>'AUTO', cascade=>TRUE, no_invalidate=>FALSE, force=>FALSE) }");
return cs;
}
},
new CallableStatementCallback() {
public Object doInCallableStatement(CallableStatement cs) throws SQLException{
cs.execute();
return null; // Whatever is returned here is returned from the jdbcTemplate.execute method
}
}
);
Came up with a decent solution to this today, that copes with non-null defaults, and does not use fruity reflection techniques. It works by creating the metadata context for the function externally to retrieve all the parameter types and so forth, then constructing the SimpleJdbcCall manually from that.
First, create a CallMetaDataContext for the function:
CallMetaDataContext context = new CallMetaDataContext();
context.setFunction(true);
context.setSchemaName(schemaName);
context.setProcedureName(functionName);
context.initializeMetaData(jdbcTemplate.getDataSource());
context.processParameters(Collections.emptyList());
Next, create the SimpleJdbcCall, but force it to not do its own metadata lookup:
SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate);
// This forces the call object to skip metadata lookup, which is the part that forces all parameters
simpleJdbcCall.setAccessCallParameterMetaData(false);
// Now go back to our previously created context and pull the parameters we need from it
simpleJdbcCall.addDeclaredParameter(context.getCallParameters().get(0));
for (int i = 0; i < params.length; ++i) {
simpleJdbcCall.addDeclaredParameter(context.getCallParameters().get(i));
}
// Call the function and retrieve the result
Map<String, Object> resultsMap = simpleJdbcCall
.withSchemaName(schemaName)
.withFunctionName(functionName)
.execute(params);
Object returnValue = resultsMap.get(context.getScalarOutParameterName());
I found solution for my case with SimpleJdbcCall and Spring 5.2.1, Java 8, Oracle 12.
You need to:
Use .withoutProcedureColumnMetaDataAccess()
Use .withNamedBinding()
Declare parameters, you know about in .declareParameters() call. Procedure will be called only with parameters, declared in this method. Default parameters, you dont want to set, arent writing here.
Example call is below
final String dataParamName = "P_DATA";
final String ageParamName = "P_AGE";
final String genderParamName = "P_GENDER";
final String acceptedParamName = "P_ACCEPTED";
SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(getJdbcTemplate())
.withCatalogName("PKG_USER")
.withProcedureName("USER_CHECK")
.withoutProcedureColumnMetaDataAccess()
.withNamedBinding()
.declareParameters(
new SqlParameter(dataParamName, OracleTypes.VARCHAR),
new SqlParameter(ageParamName, OracleTypes.NUMBER),
new SqlParameter(genderParamName, OracleTypes.VARCHAR),
new SqlOutParameter(acceptedParamName, OracleTypes.NUMBER)
);
SqlParameterSource parameterSource = new MapSqlParameterSource()
.addValue(dataParamName, data)
.addValue(ageParamName, age)
.addValue(genderParamName, gender);
Map<String, Object> out = simpleJdbcCall.execute(parameterSource);
Here is a different approach that I have taken. I added the ability for the user to set the number of parameters they will be providing on the call. These will be the first n number of positional parameters. Any remaining parameters available in the stored-proc, will have to be set via the database's default value handling. This allows new parameters to be added to the end of the list with default values, or to be null-able, without breaking code that does not know to provide a value.
I sub-classed SimpleJdbcCall and added the methods to set the "maxParamCount". I also used a bit a evil reflection to set my sub-classed version of CallMetaDataContext.
public class MySimpleJdbcCall extends SimpleJdbcCall
{
private final MyCallMetaDataContext callMetaDataContext = new MyCallMetaDataContext();
public MySimpleJdbcCall(DataSource dataSource)
{
this(new JdbcTemplate(dataSource));
}
public MySimpleJdbcCall(JdbcTemplate jdbcTemplate)
{
super(jdbcTemplate);
try
{
// Access private field
Field callMetaDataContextField = AbstractJdbcCall.class.getDeclaredField("callMetaDataContext");
callMetaDataContextField.setAccessible(true);
// Make it non-final
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(callMetaDataContextField, callMetaDataContextField.getModifiers() & ~Modifier.FINAL);
// Set field
callMetaDataContextField.set(this, this.callMetaDataContext);
}
catch (NoSuchFieldException | IllegalAccessException ex)
{
throw new RuntimeException("Exception thrown overriding AbstractJdbcCall.callMetaDataContext field", ex);
}
}
public MySimpleJdbcCall withMaxParamCount(int maxInParamCount)
{
setMaxParamCount(maxInParamCount);
return this;
}
public int getMaxParamCount()
{
return this.callMetaDataContext.getMaxParamCount();
}
public void setMaxParamCount(int maxInParamCount)
{
this.callMetaDataContext.setMaxParamCount(maxInParamCount);
}
}
In my CallMetaDataContext sub-class, I store the maxInParamCount, and use it to trim the list of parameters known to exist in the stored-proc.
public class MyCallMetaDataContext extends CallMetaDataContext
{
private int maxParamCount = Integer.MAX_VALUE;
public int getMaxParamCount()
{
return maxParamCount;
}
public void setMaxParamCount(int maxInParamCount)
{
this.maxParamCount = maxInParamCount;
}
#Override
protected List<SqlParameter> reconcileParameters(List<SqlParameter> parameters)
{
List<SqlParameter> limittedParams = new ArrayList<>();
int paramCount = 0;
for(SqlParameter param : super.reconcileParameters(parameters))
{
if (!param.isResultsParameter())
{
paramCount++;
if (paramCount > this.maxParamCount)
continue;
}
limittedParams.add(param);
}
return limittedParams;
}
}
Use is basically the same except for seeting the max parameter count.
SimpleJdbcCall call = new MySimpleJdbcCall(jdbcTemplate)
.withMaxParamCount(3)
.withProcedureName("MayProc");
SMALL RANT: It's funny that Spring is well know for its IOC container. But, within its utility classes, I have to resort to reflection to provide an alternate implementation of a dependent class.
Was also struggling with the problem, and didn't want to deal with strings.
There could be more interesting solution, if we get default values from meta data, which spring doesn't care about in default implementation, but I simply put nulls there.
The solution came like the following:
Overridden simpleJdbcCall
private class JdbcCallWithDefaultArgs extends SimpleJdbcCall {
CallableStatementCreatorFactory callableStatementFactory;
public JdbcCallWithDefaultArgs(JdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}
#Override
protected CallableStatementCreatorFactory getCallableStatementFactory() {
return callableStatementFactory;
}
#Override
protected void onCompileInternal() {
callableStatementFactory =
new CallableStatementCreatorWithDefaultArgsFactory(getCallString(), this.getCallParameters());
callableStatementFactory.setNativeJdbcExtractor(getJdbcTemplate().getNativeJdbcExtractor());
}
#Override
public Map<String, Object> execute(SqlParameterSource parameterSource) {
((CallableStatementCreatorWithDefaultArgsFactory)callableStatementFactory).cleanupParameters(parameterSource);
return super.doExecute(parameterSource);
}
}
And overriden CallableStatementCreatorFactory
public class CallableStatementCreatorWithDefaultArgsFactory extends CallableStatementCreatorFactory {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final List<SqlParameter> declaredParameters;
public CallableStatementCreatorWithDefaultArgsFactory(String callString, List<SqlParameter> declaredParameters) {
super(callString, declaredParameters);
this.declaredParameters = declaredParameters;
}
protected void cleanupParameters(SqlParameterSource sqlParameterSource) {
MapSqlParameterSource mapSqlParameterSource = (MapSqlParameterSource) sqlParameterSource;
Iterator<SqlParameter> declaredParameterIterator = declaredParameters.iterator();
Set<String> parameterNameSet = mapSqlParameterSource.getValues().keySet();
while (declaredParameterIterator.hasNext()) {
SqlParameter parameter = declaredParameterIterator.next();
if (!(parameter instanceof SqlOutParameter) &&
(!mapContainsParameterIgnoreCase(parameter.getName(), parameterNameSet))) {
logger.warn("Missing value parameter "+parameter.getName() + " will be replaced by null!");
mapSqlParameterSource.addValue(parameter.getName(), null);
}
}
}
private boolean mapContainsParameterIgnoreCase(String parameterName, Set<String> parameterNameSet) {
String lowerParameterName = parameterName.toLowerCase();
for (String parameter : parameterNameSet) {
if (parameter.toLowerCase().equals(lowerParameterName)) {
return true;
}
}
return false;
}
#Override
public void addParameter(SqlParameter param) {
this.declaredParameters.add(param);
}
I use this util method:
public <T> void setOptionalParameter(MapSqlParameterSource parameters, String name, T value) {
if (value == null)
parameters.addValue(name, value, Types.NULL);
else
parameters.addValue(name, value);
}