how to pass object parameters in command? - java

I created an eclipse-rcp's project's plugin.xml with a new command with a parameter.
ArrayList<parameterization> parameters = new ArrayList<parameterization>();
IParameter iparam;
//get the command from plugin.xml
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
ICommandService cmdService = (ICommandService)window.getService(ICommandService.class);
Command cmd = cmdService.getCommand("org.ipiel.demo.commands.click");
//get the parameter
iparam = cmd.getParameter("org.ipiel.demo.commands.click.paramenter1");
Parameterization params = new Parameterization(iparam, "commandValue");
parameters.add(params);
//build the parameterized command
ParameterizedCommand pc = new ParameterizedCommand(cmd, parameters.toArray(new Parameterization[parameters.size()]));
//execute the command
IHandlerService handlerService = (IHandlerService)window.getService(IHandlerService.class);
handlerService.executeCommand(pc, null);
I tried this example to pass parameters and it worked.
The issue in this example that I could pass only parameters of type String. ( because Parameterization )
I want to pass parameter of hash map and in general to pass any object.
I tried this code
IServiceLocator serviceLocator = PlatformUI.getWorkbench();
ICommandService commandService = (ICommandService) serviceLocator.getService(ICommandService.class);
ExecutionEvent executionEvent = new ExecutionEvent(cmd, paramArray, null, null);
cmd.executeWithChecks(executionEvent);
but it didn't work the parameters didn't move ( it was null)
Could you please help to to move object as parameter in command ?

Since it would get confusing to add another solution to my first answer, I'll provide another one for a second solution.
The choices I gave were " A) use the selected object of the "Execution Event" (examine that, it contains a lot of infos). B) you can use AbstractSourceProvider, so you can pass your object to the application context."
A) can be used in your Handler if your object is the selection of a Structured Object like a Tree:
MyObject p = (MyObject) ((IStructuredSelection) HandlerUtil.getCurrentSelection(event)).getFirstElement();
B) The usage of a Source provider is a bit more tricky. The main idea is, that you add your object to the application context. The important snippets for Eclipse 3.x from a project that I set up after I read this blog (note: it is in german and the example it provides doesn't work):
In your plugin.xml add:
<extension point="org.eclipse.ui.services">
<sourceProvider
provider="com.voo.example.sourceprovider.PersonSourceProvider">
<variable
name="com.voo.example.sourceprovider.currentPerson"
priorityLevel="activePartId">
</variable>
</sourceProvider>
Set up your own SourceProvider. Calling the "getCurrentState" you can get the variable (your Person object in this case) of that SourceProvider:
public class PersonSourceProvider extends AbstractSourceProvider{
/** This is the variable that is used as reference to the SourceProvider
*/
public static final String PERSON_ID = "com.voo.example.sourceprovider.currentPerson";
private Person currentPerson;
public PersonSourceProvider() {
}
#Override
public void dispose() {
currentPerson = null;
}
**/**
* Used to get the Status of the source from the framework
*/
#Override
public Map<String, Person> getCurrentState() {
Map<String, Person> personMap = new HashMap<String, Person>();
personMap.put(PERSON_ID, currentPerson);
return personMap;
}**
#Override
public String[] getProvidedSourceNames() {
return new String[]{PERSON_ID};
}
public void personChanged(Person p){
if (this.currentPerson != null && this.currentPerson.equals(p)){
return;
}
this.currentPerson = p;
fireSourceChanged(ISources.ACTIVE_PART_ID, PERSON_ID, this.currentPerson);
}
}
In your View you register to the SourceProvider and set the Object to the object you want to transfer to your Handler.
public void createPartControl(Composite parent) {
viewer = new TreeViewer(parent);
viewer.setLabelProvider(new ViewLabelProvider());
viewer.setContentProvider(new ViewContentProvider());
viewer.setInput(rootPerson);
getSite().setSelectionProvider(viewer);
viewer.addSelectionChangedListener(new ISelectionChangedListener() {
#Override
public void selectionChanged(SelectionChangedEvent event) {
Person p = null;
if (event.getSelection() instanceof TreeSelection) {
TreeSelection selection = (TreeSelection) event.getSelection();
if (selection.getFirstElement() instanceof Person) {
p = (Person) selection.getFirstElement();
}
}
if (p==null) {
return;
}
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
ISourceProviderService service = (ISourceProviderService) window.getService(ISourceProviderService.class);
PersonSourceProvider sourceProvider = (PersonSourceProvider) service.getSourceProvider(PersonSourceProvider.PERSON_ID);
sourceProvider.personChanged(p);
}
});
}
And in your Handler you can just call the PersonSourceProvider#getCurrentState to get your Objects back.
Advantage of this method is, that you can use the Objectd anywhere you want. E.g. you can even set up a PropertyTester to enable/disable UI elements according to the currently selected Object.

The Parameterized Command does only accept Strings.
Here is an example for smaller objects:
Disclaimer: this is for Eclipse 3.x. I am not using Eclipse 4.x a lot, so you might have to adapt there in case you need it.
Create a Pluginproject (com.voo.example.commandparameter.advanced) with a View (com.voo.example.commandparameter.advanced.view) , a Command (com.voo.example.commandparameter.advanced.sysoCommand) with menu entry and Handler(com.voo.example.commandparameter.advanced.sysoCommand), and a universal Object (MyTestObject).
The Command needs a Parameter and a Parametertype in the plugin.xml, that gets passed to it:
<extension
point="org.eclipse.ui.commands">
<command
id="com.voo.example.commandparameter.advanced.sysoCommand"
name="SysoCommand">
<commandParameter
id="myObject"
name="object"
optional="true"
typeId="com.voo.example.commandparameter.advanced.testType">
</commandParameter>
</command>
<commandParameterType
id="com.voo.example.commandparameter.advanced.testType"
type="com.voo.example.commandparameter.advanced.MyTestObject">
</commandParameterType>
In the Object you set atrtibutes like name and street and define a convertToString method like that:
public String convertToString() {
return getName() +",,,"+ getStreet();
}
(you can override the toString method, too. I just used that method to set weired delimiters to the returned String)
And in a Class MyParamterConverter you can transfer it back:
public class MyParameterConverter extends AbstractParameterValueConverter {
public MyParameterConverter() {
}
#Override
public String convertToString(Object parameterValue)
throws ParameterValueConversionException {
return parameterValue.toString();
}
/**
* This will always create a new object. Just keep that in mind
* if you're trying to work with the objects.
*/
#Override
public Object convertToObject(String parameterValue)
throws ParameterValueConversionException {
//Split the String to get the attributes back
String delimiter =",,,";
String[] split = parameterValue.split(delimiter);
String name = split[0];
String street = split [1];
return new MyTestObject(name, street);
}
}
Now you can call the command with a buttonclick in your view, for example:
btnGo.addSelectionListener(new SelectionAdapter() {
#Override
public void widgetSelected(SelectionEvent event) {
MyTestObject testObject = new MyTestObject(textName.getText(),textStreet.getText());
ICommandService cS = (ICommandService)getSite().getService(ICommandService.class);
IHandlerService hS = (IHandlerService)getSite().getService(IHandlerService.class);
Command sysoComm = cS.getCommand("com.voo.example.commandparameter.advanced.sysoCommand");
HashMap<String, String> params = new HashMap<String, String>();
params.put("myObject", testObject.convertToString());
ParameterizedCommand pC = ParameterizedCommand.generateCommand(sysoComm, params);
try {
hS.executeCommand(pC, null);
} catch (Exception e) {
e.printStackTrace();
}
}
});
And the Handler can transform the passed parameters back :
public class MyObjectHandler extends AbstractHandler {
#Override
public Object execute(ExecutionEvent event) throws ExecutionException {
String param1 = event.getParameter("myObject");
MyParameterConverter converter = new MyParameterConverter();
Object convertToObject = null;
try {
convertToObject = converter.convertToObject(param1);
} catch (ParameterValueConversionException e) {
e.printStackTrace();
}
if (convertToObject instanceof MyTestObject) {
MyTestObject to = (MyTestObject) convertToObject;
System.out.println(to.toString());
}
return null;
}
}
This should work for most smaller sized objects that do not change while you pass them. If you need to pass bigger objects, you will have two choices: A) use the selected object of the "Execution Event" (examine that, it contains a lot of infos). B) you can use AbstractSourceProvider, so you can pass your object to the application context.

For a long time I have been focused on delivering an object via a command parameter. But in the end, the easiest workaround is to simply ignore the parameter stuff and put the desired object in a new child IExclipseContext and execute the command with that context. That way your handler gets your object injected.
Caller:
ECommandService commandService = // get commandService...
EHandlerService handlerService = // get handlerService...
IEclipseContext context = // get active or application context...
IEclipseContext childCtx = context.createChild();
childCtx.set(MyObject.class, instancOfMyObject);
ParameterizedCommand command = commandService.createCommand("my.command.id", null);
handlerService.executeHandler(command, childCtx);
In your handler:
#Execute
public void execute(#Optional MyObject myObject) {
if(myObject != null) {
// work with your object
}
}
Voila, no converters or callbacks (i.e. SelectionService) needed...

I am not really familiar with this as passing parameters to commands is quite rare. It looks like you have to use commandParameterType in the org.eclipse.ui.commands command definition to define code based on AbstractParameterValueConverter to convert between objects and the string for the parameter value.

Related

Pass an Object Type variable inside a method and cast it to a specific class inside if statement

I pass an Object Type variable inside a method and i want to cast it to a specific class according to a flag (I am also passing the flag).
If I put the code inside the if - else statement it works, but i end up with duplicate code.
This is an existing project and I cannot mess with the objects.
public void insertReport(Object request , String requestJson , int reportFlag){
Object reportRequest;
if (reportFlag == 0 ) {
reportRequest = (MonthlyCls) request;
}else{
reportRequest = (DailyCls) request;
}
RepEntity repEntity = new RepEntity ();
repEntity.setId(reportRequest.getReportInfo().getId());
repEntity.setDate(newTimestamp(reportRequest.getReportInfo().getDate()));
Is there a way to make java "understands" the casting in compile time?
Thank you very much in advance.
Simply use an interface or an abstract class to have a common type.
public abstract class Request{
public abstract ReportingInfo getReportInfo();
}
Then Extends the class in both classes, the methods should already be implemented.
public class DailyCls extends Request {
public ReportingInfo(){ ... }
}
public class MonthlyCls extends Request {
public ReportingInfo(){ ... }
}
You can also implement the method in Request if it is possible/necessary.
That way, you just have to change the signature to accept a Request
public void insertReport(Request request, String requestJson){
RepEntity repEntity = new RepEntity ();
repEntity.setId(request.getReportInfo().getId());
repEntity.setDate(newTimestamp(request.getReportInfo().getDate()));
}
First thing first, even if you cast the object to either MonthlyCls or DailyCls, the variable reportRequest is of type Object, so casting that object will not do anything. In order to be able to "access" the specific methods of both classes, you need to write something like this:
public void insertReport(Object request , String requestJson , int reportFlag){
MonthlyCls reportRequestMonthly = null;
DailyCls reportRequestDaily = null;
if (reportFlag == 0 ) {
reportRequestMonthly = (MonthlyCls) request;
}else{
reportRequestDaily = (DailyCls) request;
}
RepEntity repEntity = new RepEntity ();
if (reportRequestMonthly != null){
repEntity.setId(reportRequestMonthly .getReportInfo().getId());
repEntity.setDate(new Timestamp(reportRequestMonthly .getReportInfo().getDate()));
} else {
repEntity.setId(reportRequestDaily .getReportInfo().getId());
repEntity.setDate(new Timestamp(reportRequestDaily .getReportInfo().getDate()));
}
EDIT: I am assuming that both the objects are not related in any way by a SuperClass or anything, I suggest you do so and you also check with instanceof if the object given is correct.
If you can update the classes existing, a solution would be to implement an adapter that would use the reflection to call the methods.
Something like this would be quite safe to use
class RequestAdapter{
private Object request;
public RequestAdapter(Object request){
if(request == null) throw new IllegalArgumentException("The request can't be null");
if(!isSupported(request)) throw new IllegalArgumentException("Type not supported : " + request.getClass().getName());
this.request = request;
}
// call the method "getReportingInfo" by reflection on the object
public ReportingInfo getReportingInfo(){
try {
return (ReportingInfo) request.getClass().getMethod("getReportingInfo").invoke(request);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException
| SecurityException e) {
e.printStackTrace();
return null;
}
}
static Class<?>[] supportedType = {
DailyCls.class,
MonthlyCls.class
};
//Check if the type is supported, to prevent any mistake with the reflection call later.
private boolean isSupported(Object request){
for(Class<?> c : supportedType){
if(c == request.getClass()){
return true;
}
}
return false;
}
}
This is really not a clean solution, but if the method public ReportingInfo getReportingInfo() is defined in every class you add in the supportedType array. This would be quite safe.
Tested with :
public void insertReport(Object request){
RepEntity repEntity = new RepEntity ();
RequestAdapter adapter = new RequestAdapter(request);
repEntity.setId(adapter.getReportInfo().getId());
repEntity.setDate(newTimestamp(adapter.getReportInfo().getDate()));
}
If anything else than a MonthlyCls or DailyCls is pass to the method, you will get an IllegalArgumentException :
Like this : new RequestAdapter("");
Exception in thread "main" java.lang.IllegalArgumentException: Type not supported : java.lang.String

How to dynamically handle commands in a chat program?

My question is more of a design issue than anything else. I'm currently developing a classic Server-Client chat program in Java. Everything is fine until I get to the commands. I thought it would be convenient for users to send commands that would then be treated by the server for changing their nickname for example. The thing is I want to make flexible code and above all, object-oriented code. To avoid endless if/else if statements to know what command was typed I believe it would be better to create a class for each command which inherit from a superclass Command. Then I could return the specific command through a getCommand() function overriden in all subclasses. But it does not solve my problem at all. The server still needs to test with instanceof what command has been returned. One way to do it dynamically would be to sort of auto downcasting it from the superclass Command and then call the appropriate function in the server class. For example:
public void processCommand(CommandNick c) {}
public void processCommand(CommandKick c) {}
But I haven't found any proper way of doing that and even if I did, I feel like there's still a design issue here. And I am convinced there is a nice and flexible way to do it but days weren't enough for me to figure it out. Any ideas? Thanks in advance! :)
I assume your server receives the message as an Object with a Sender and a String. Create your Command classes, and in the server init code, make a HashMap<String, AbstractCommand> with a String as key and your AbstractCommand class as value. Your commands should extend this class. Register all your commands, like so:
commandRegistry.put("help", new HelpCommandHandler());
I assume a command is a message with a ! before it. So when you receive a message, check if it is a command:
Message message = (Your Message)
String messageBody = message.getBody();
Sender messageSender = message.getSender();
if(messageBody.startsWith("!")) {
// Split the message after every space
String[] commandParts = messageBody.split(" ");
// The first element is the command base, like: !help
String baseCommand = commandParts[0];
// Remove the first character from the base, turns !help into help
baseCommand = baseCommand.substring(1, baseCommand.length());
// Creates a new array for the arguments. The length is smaller, because we won't copy the command base
String[] args = new String[commandParts.length - 1];
// Copy the elements of the commandParts array from index 1 into args from index 0
if(args.length > 0) {
System.arraycopy(commandParts, 1, args, 0, commandParts.length - 1);
}
// Your parse method
processCommand(sender, baseCommand, args);
}
public void processCommand(Sender sender, String base, String[] args) {
if(commandRegistry.containsKey(base)) {
commandRegistry.get(base).execute(sender, args);
} else {
// Handle unknown command
}
}
public abstract class AbstractCommand {
public abstract void execute(Sender sender, String[] args);
}
Sample implementation. I assume your server is a Singleton, and you can get on Object of it with Server.get() or any similar method.
public class HelpCommandHandler extends AbstractCommand { /* !help */
#Override
public void execute(Sender sender, String[] args) {
sender.sendMessage("You asked for help."); // Your code might not work like this.
}
}
public class ChangeNickCommandHandler extends AbstractCommand { /* !changenick newNick */
#Override
public void execute(Sender sender, String[] args) {
// I assume you have a List with connected players in your Server class
String username = sender.getUsername(); // Your code might not work like this
Server server = Server.get(); // Get Server instance
server.getUsers().get(username).setNickname(args[0]); // Argument 0. Check if it even exists.
}
}
// Server class. If it isn't singleton, you can make it one like this:
public class Server {
private static Server self;
public static Server init(/* Your args you'd use in a constructor */) { self = new Server(); return get(); }
public static Server get() { return self; }
private List<User> users = new List<User>();
private HashMap<String, AbstractCommand> commandRegitry = new HashMap<>();
// Make construcor private, use init() instead.
private Server() {
commandRegistry.put("help", new HelpCommandHandler());
commandRegistry.put("changenick", new ChangeNickCommandHandler());
}
// Getters
public List<User> getUsers() {
return users;
}
public HashMap<String, AbstractCommand> getRegistry() {
return commandRegistry;
}
}
This is a bit of pseudo code to illustrate that your controller doesn't need to know about the command processors (no need for instanceof).
abstract class CommandProcessor {
/* return boolean if this Command processed the request */
public static boolean processCommand(String command, User user, Properties chatProperties, Chat chat);
}
/* Handle anything */
public class CommandRemainder extends CommandProcessor {
#Override
public static boolean processCommand(String command, User user, Properties chatProperties, Chat chat) {
chat.appendText("[" + user.getName() + "] " + command);
return true;
}
}
/* Handle color changing */
public class CommandColorizer extends CommandProcessor {
protected static List<String> ALLOWED_COLORS = new ArrayList<>(Arrays.asList("red", "blue", "green"));
#Override
public static boolean processCommand(String command, User user, Properties chatProperties, Chat chat) {
if ("fg:".equals(command.trim().substring(0,3)) {
String color = command.trim().substring(3).trim();
if (ALLOWED_COLORS.contains(color)) {
chat.setForeground(color);
}
return true;
}
return false;
}
}
public class ChatController {
protected Chat chat = new Chat();
protected User user = getUser();
protected Properties chatProperties = getChatProperties();
protected List<CommandProcessor> commandProcessors = getCommandProcessors();
{
chat.addChatListener(new ChatListener(){
#Override
public void userChatted(String userChatString) {
for (CommandProcessor processor : commandProcessors) {
if (processor.processCommand(userChatString, user, chatProperties, chat)) {
break;
}
}
}
});
}
List<CommandProcessor> getCommandProcessors() {
List<CommandProcessor> commandProcessors = new ArrayList<>();
commandProcessors.add(new CommandColorizer());
commandProcessors.add(new CommandRemainder()); // needs to be last
return commandProcessors;
}
}

Create Class from String and use Command Pattern

I know this has been questioned before on stackoverflow, but I could not find a solution for my problem.
I want to separate some Code from ButtonClick event, where I dynamically create Nodes on ButtonClick and add them to a parent AnchorPane. On my Nodes are Buttons, these Events on my Button are handled with a CommandPattern. I create different ButtonEvents, depending on the Node I created.
The Code which works fine so far is:
#FXML
void addErosionNode(ActionEvent event){
DragNode nde = new DragNode();
/*
id = nde.getId();
name = new String("Erosion");
Erosion cmd;
cmd = new Erosion();
nodeList.add(new NodeList<String, String, Command>(name, id, cmd));
*/
setupNode(nde);
nde.setNodeWithTwoCircles();
}
But I want to put the Code between /**/ inside another Method, so I can replace the Code with setupNode(nde, name);
And try to use this method:
public void setupNode(DragNode nde, String name){
id = nde.getId();
Class clazz;
className = new String ("application.bvfunc." + type);
//This will be e.g. application.bvfunc.Erosion which is the class I want to use
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
System.out.println("fail");
}
clazz cmd;
cmd = new clazz();
nodeList.add(new NodeList<String, String, Command>(name, id, cmd));
nde.nodeLayout();
rightAnchor.getChildren().add(nde);
buildDragHandlers();
}
But clazz cannot be resolved to a type.
How can I replace
Erosion cmd;
cmd = new Erosion();
with clazz I create with the name of my Node?
Like this:
clazz cmd;
cmd = new clazz();
You can, instead of using reflection, create a map that maps the names of their commands to the constructors:
private static final Map<String, Supplier<Command>> commandMap = new HashMap<>();
static {
commandMap.put("Erosion", Erosion::new);
commandMap.put("Dilation", Dilation::new);
}
Then in your other code:
public void setupNode(DragNode nde, String name){
id = nde.getId();
Command cmd = commandMap.get(name).get(); // <-- Calls the constructor
nodeList.add(new NodeList<String, String, Command>(name, id, cmd));
nde.nodeLayout();
rightAnchor.getChildren().add(nde);
buildDragHandlers();
}
Replace
clazz cmd;
cmd = new clazz();
with
Command cmd = (Command) clazz.newInstance();

Comparing a String to an Object's toString() method dynamically

I'm reading from a text file, like so:
while ((line = br.readLine()) != null) {
String[] a = line.split("\\: ");
key = a[0];
action = a[1];
gameKeys.add(key, action);
}
where the file would be something like
SPACE: FIRE_ACTION
E: USE_ACTION
This part works, key and action are both what I want.
gameKeys is a Map declared like so:
private static Map<Keyboard.Key, Action> gameKeys = new HashMap<>();
Keyboard.Key has fields such as SPACE, A, RETURN, etc.
Action is an interface, that holds other actions; those actions have a toString() method that returns the action, e.g. new FireAction.toString() returns FIRE_ACTION.
Example of an Action:
public class FireAction implements Action {
#Override
public void execute() {
System.out.println("Fire key pressed!");
}
#Override
public String toString() {
return "FIRE_ACTION";
}
}
So, I'm trying to turn the file's components into objects, like if key was "SPACE" and action was "FIRE_ACTION", then, after the add method is performed, gameKeys would have <Keyboard.Key.SPACE, new FireAction()>
Is there anyway I can do this?
You could try this:
Save your Action classes in a Map<String, Class<? extends Action>>
Read the Key -> Action bindings from the file
Resolve the string action to an actual Action object via the map
Example:
public class Main {
private static final Map<Keyboard.Key, Action> gameKeys = new HashMap<>();
private static final Map<String, Class<? extends Action>> actions = new HashMap<>();
static {
actions.put(FireAction.NAME, FireAction.class);
actions.put(WalkAction.NAME, WalkAction.class);
}
public static void main(String[] args) {
// read from file etc.
try {
// e.g. found SPACE : FIRE_ACTION
gameKeys.put(Keyboard.Key.SPACE, actions.get("FIRE_ACTION").newInstance());
// e.g. found A : WALK_ACTION
gameKeys.put(Keyboard.Key.A, actions.get("WALK_ACTION").newInstance());
} catch (IllegalAccessException | InstantiationException ex) {
ex.printStackTrace();
}
}
}
public class FireAction implements Action {
public static final String NAME = "FIRE_ACTION";
#Override
public void execute() {
System.out.println("Fire key pressed!");
}
#Override
public String toString() {
return NAME;
}
}
Sure
Object keyObj = key, actionObj;
if (key.equals("SPACE")) keyObj = Keyboard.Key.SPACE;
if (action.equals("FIRE_ACTION")) actionObj = new FireAction());
You can use a Map<String, ...> as an alternative to using 'if's if you have a lot of cases
You can't achieve what you have asked directly - because then Java would have to create ALL the classes it can create(some have non-default constructors or even private), and call their toString() method (which may have side-effects in general case).
So anyway you'll have to create registry with all actions(preferrable way), or you can try to use reflection to create Actions in runtime.

Spring SimpleJdbcCall default (optional) arguments

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);
}

Categories

Resources