22 May 2011

Dynamic ADF Train. Adding train stops programmatically.

In this post I'm going to show how to add train stops to ADF train programmatically "on-the-fly". In my use-case I have some ticket-booking application. It has a bounded task flow with train model. At the first stop of the train users input number of passengers and at the following stops they input some passengers' info. The number of stops with passengers' info has to be changed dynamically depending on the value submitted at the first train stop. So, the result of described behaviour should look like this:


The bounded task flow has the following structure:
StartView activity is a page fragment where we input number of passengers and DynamicView activity provides a page fragment to input passenger's info. At the moment we have only one activity for passenger's info and I will add extra activities if the number of passengers is greater than one.
The inputNumberSpinbox in StartView page fragment submits its value to passengersNumber property of some PageFlowScope backing bean and action for the Submit button is a method of the same bean:

public class MainTrain {
    //Extra added train stops
    private List<ActivityId> dynamicStops = new ArrayList<ActivityId>();
    
    //Value of inputNumberSpinbox
    private int passengersNumber = 1;
    
    public String buttonPress(){
        //The number of extra added train stops is greater than needed
        if (passengersNumber <= dynamicStops.size())
            clearExtraStops();
        else //The number of extra added train stops is less than needed        
          if (passengersNumber-1 > dynamicStops.size()) 
              addDynamicStops(); 
        return null;
    }

So, by pressing on Submit button we either add some train stops or clear extra stops depending on the value of  inputNumberSpinbox. We save all added dynamic stops in dynamicStops list. Let's have a look at the clearExtraStops() method:
    private void clearExtraStops() {
        for (int i = dynamicStops.size(); i >= passengersNumber; i--) {
            //Get ActivityId to be removed
            ActivityId removeActivityId =  dynamicStops.get(i-1);

            //Get current train model and remove train stop
            TrainModel trainModel = TrainUtils.findCurrentTrainModel();
            trainModel.getTrainStops().remove(removeActivityId);
            
            //Remove activity from task flow definition
            getTaskFlowDefinition().getActivities().remove(removeActivityId);
            dynamicStops.remove(i-1);
        }                            
    }

The method removes two things: the train stop from the train model and the activity from the task flow definition. The addDynamicStops() method is going to be much more interesting:
private void addDynamicStops() {    
    for (int i = dynamicStops.size(); i < passengersNumber - 1; i++) {
       //Creating new ActivityId
       ActivityId activityId = 
           new ActivityId(getTaskFlowId(), new StringBuilder("DynamicView").append(i).toString()); 

       //The main trick of the post.
       //We consider DynamicView activity as a base for new train stop and new activity
           
       //Get base activity (DynamicView) and its train stop
       Activity baseActivity = getBaseDynamicActivity();
       TrainStopContainer stopContainer = (TrainStopContainer)baseActivity.getMetadataObject();
       TrainStop baseTrainStop = stopContainer.getTrainStop();

       //Create new Activity based on DynamicView but with new ActivityId            
       ActivityImpl activityImpl = new ActivityImpl(baseActivity, activityId);  
       //Add created activity to the task flow definition
       getTaskFlowDefinition().getActivities().put(activityId, activityImpl);

       //Create new train stop based on the DynamicView's train stop
       TrainStopModel trainStopModel = new TrainStopModel(
                          new TrainStopImpl(baseTrainStop, i+2), activityId);
       //Add created train stop to the train stop model
       TrainModel trainModel = TrainUtils.findCurrentTrainModel();
       trainModel.getTrainStops().put(activityId, trainStopModel);             
       //Add created activity to our list
       dynamicStops.add(activityId); 
    }
}
    
private Activity getBaseDynamicActivity() {
   ActivityId baseActivityId = new ActivityId(getTaskFlowId(), "DynamicView");   
   MetadataService metadataService = MetadataService.getInstance();
   return metadataService.getActivity(baseActivityId); 
}

private TaskFlowDefinition getTaskFlowDefinition() {
    MetadataService metadataService = MetadataService.getInstance();
    return metadataService.getTaskFlowDefinition(getTaskFlowId());        
}


private TaskFlowId getTaskFlowId() {
    ControllerContext controllerContext = ControllerContext.getInstance(); 
    ViewPortContext currentViewPortCtx = controllerContext.getCurrentViewPort(); 
    TaskFlowContext taskFlowCtx = currentViewPortCtx.getTaskFlowContext(); 
    return taskFlowCtx.getTaskFlowId();
}

So, the principal trick of this post is to create new activity and train stops basing on existing ones for DynamicView. In order to implement the idea I created two classes: ActivityImpl and TrainStopImpl. The classes are nothing else than just proxy classes implementing Activity and TrainStop interfaces correspondently. They delegates interface implementation to the base instances except some specific methods like getters for Id and DisplayName:

public class TrainStopImpl implements TrainStop {    
    //Base instance 
    private TrainStop baseTrainStop;
    
    private int mpassNo;
    private static final String PASSANGER_FORM = "Passenger's data: ";
    
    public TrainStopImpl(TrainStop trainStop, int passNo) {
       baseTrainStop = trainStop; 
       mpassNo = passNo;
    }

    //Specific implementation
    public String getDisplayName() {
        return new StringBuilder(PASSANGER_FORM).append(mpassNo).toString();
    }

    public String getOutcome() {
        return baseTrainStop.getOutcome();
    }

    public String getSequential() {
        return baseTrainStop.getSequential();
    }

...

public class ActivityImpl implements Activity {
    private Activity baseActivity;
    private ActivityId mid;
    
    public ActivityImpl(Activity activity, ActivityId id) {
        baseActivity = activity;
        mid = id;
    }

    //Specific implementation
    public ActivityId getId() {
        return mid;
    }

    public String getType() {
        return baseActivity.getType();
    }

    public Object getMetadataObject() {
        return baseActivity.getMetadataObject();
    }
...
 

And one more picture for this post, just to show it's working:

That's all! You can download sample application for JDeveloper 11.1.1.2.0.

8 May 2011

InputText with suggestion on demand

Introduction 
ADF Faces provide us by quite handy tag af:autoSuggestBehavior. It could be used together with some input control in order to implement very common use-case when  a user typing some text is suggested by a drop-down list with some values to be selected and displayed in the input field. The following screenshot shows an example of such behavior:

The jspx code for this example is very simple:

        <af:inputText label="Currency" id="i21">
          <af:autoSuggestBehavior 
              suggestedItems="#{InputSuggestBean.onCurrencySuggest}"/>
        </af:inputText>

The af:autoSuggestBehavior tag needs to be bounded to a backing bean method returning a list of items to be suggested. The method has only one String argument containing submitted user's value. Depending on this value you can implement your own logic for the returning list like filtering, sorting, etc. For example, the following backing bean method returns currency codes that match user's input string:

    private final static String[] ccys = {"USD", "EUR", "UAH", "CAD"};

    public List onCurrencySuggest(String inputValue) {
        ArrayList<SelectItem> suggestItems = new ArrayList<SelectItem>();
        for (String s: ccys) {
            if (s.startsWith(inputValue)) 
                suggestItems.add(new SelectItem(s));
        }
        return suggestItems;
    }

Of course, you can implement your backing bean method getting suggested items from ADF BC layer in order to retrieve them from database or from other data sources. You can find detailed example of this feature in the ADF Code Corner Article posted by Frank Nimphius.

Suggestion on demand
But there is another common use-case when users don't need any auto-suggestion for their input, and they want to be suggested on demand only. For example, by pressing "Ctrl-H", a user is provided by previously submitted values for this input field. Something like a history of values.
In this post I'm going to show two different implementations of this use-case. The first one is based on modified af:autoSuggestBehavior tag and the second implementation is built using usual af:popup tag.

Modified af:autoSuggestBehavior
  The jspx definition of inputText looks as usual:

        <af:inputText label="Modified input" id="it1">        
          <af:autoSuggestBehavior 
            suggestedItems="#{InputSuggestBean.onSuggest}"/>
        </af:inputText>

And the backing bean method looks like this:

    public List onSuggest(String string) {
        ArrayList<SelectItem> selectItems = new ArrayList<SelectItem>();
      
        //Let's assume these are history values:
        selectItems.add(new SelectItem("One"));
        selectItems.add(new SelectItem("Two"));        

        return selectItems;
    }

af:autoSuggestBehavior tag  renders some JavaScript object AdfAutoSuggestBehavior responsible for auto-suggestion functionality. It adds a number of event listeners to the inputText for different event types like 
onKeyUp, onBlur, onFocus, etc.

In order to prevent default behavior of af:autoSuggestBehavior tag I have to override its onKeyUp listener and do some JavaScript coding:

<af:document id="d1">      
 <af:resource type="javascript">
  var initialized = false; //Initialization flag
  
  initMySuggestBehavior = function () {
      if (!initialized) { 
          
         //Saving default _fireKeyUp function
         AdfAutoSuggestBehavior.prototype._fireKeyUpEx = 
            AdfAutoSuggestBehavior.prototype._fireKeyUp;

         //Writing proxy for _fireKeyUp function
         AdfAutoSuggestBehavior.prototype._fireKeyUp = function (e) {
           //Getting the event's source and its content                
           var input = e.getSource();
           if (input instanceof AdfUIEditableValue) 
             var inputContent = AdfDhtmlEditableValuePeer.GetContentNode(input);
             //If user has pressed ctrl H then fire suggestion
           if ("ctrl H" == e.getKeyStroke().toMarshalledString()) {
              AdfAutoSuggestBehavior.prototype._autoSuggest(input);
              //Just for better visualization
              if (inputContent.value.length == 0) inputContent.value = "...";
              }

           //This will prevent default suggestion firing for every input character
           if (inputContent) this._nodeLength = inputContent.value.length;
          
           //Calling default event listener we saved before
           AdfAutoSuggestBehavior.prototype._fireKeyUpEx(e);
        };
             
        //Looking for our inputText
        var inp = AdfPage.PAGE.findComponentByAbsoluteId("it1");
         
        /* At this moment AdfAutoSuggestBehavior has already initialized
         * and added his default listeners to the inputText.
         * We need to clear them and reinitialize with our overridden 
         * _fireKeyUp function.
         */  
        inp.setProperty("clientListeners", null);
        AdfAutoSuggestBehavior.prototype.initialize(inp);
         
        /* Adding a simple listener for onKeyDown event in order to 
         * prevent default browser behavior for Ctrl-H            
         */
        inp.addEventListener(AdfUIInputEvent.KEY_DOWN_EVENT_TYPE, myKeyDown, this);
                
        //We've done it.
        initialized = true;
     }
  }
//Preventing default browser behavior for Ctrl-H
 myKeyDown = function (event) {
    if ("ctrl H" == event.getKeyStroke().toMarshalledString())
       event.cancel();
  }
</af:resource>
...      

I'm going to call initMySuggestBehavior JavaScript  function in a phase listener written for the f:view tag:
<f:view beforePhase="#{InputSuggestBean.viewPhaseListener}">
    public void viewPhaseListener(PhaseEvent phaseEvent) {
        if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) {
          FacesContext fctx = FacesContext.getCurrentInstance();
          ExtendedRenderKitService ks = 
              Service.getRenderKitService(fctx, ExtendedRenderKitService.class);
          StringBuffer script = new StringBuffer();
          script.append("window.initMySuggestBehavior()");
          ks.addScript(fctx, script.toString());            
        }

    }

The result of our modifications looks like this:


Using af:popup

The jspx page for this implementation looks like this:

    <af:popup id="ctrlHPopup" contentDelivery="lazyUncached"
              animate="false">
      <af:selectOneListbox 
               id="sol4"
               simple="true"
               autoSubmit="false"
               >
        <f:selectItems value="#{InputSuggestBean.popupSuggestion}" id="si7"/>
        <af:clientListener type="keyDown" method="enterSelection"/>
        <af:clientListener type="click" method="clickSelection"/>
      </af:selectOneListbox>
    </af:popup>

    <af:panelLabelAndMessage label="Input with popup" id="plam1">
      <af:inputText id="it2" simple="true">
       <af:clientListener method="ctrlHKeyDown" type="keyDown"/>
      </af:inputText>
    </af:panelLabelAndMessage>

I have a popup and selectOneListbox component inside it. The listbox get suggested items from the backing bean property popupSuggestion and it has client listeners to submit selected values when Enter is pressed or mouse is clicked. The inputText has a listener to fire popup with suggested items when Ctrl-H is pressed.
The backing bean has the following method:

    public List getPopupSuggestion() {
        ArrayList<SelectItem> selectItems = new ArrayList<SelectItem>();
      
        //Let's assume these are history values
        selectItems.add(new SelectItem("The first value"));
        selectItems.add(new SelectItem("The second value"));        

        return selectItems;
    }

And for sure we have some JavaScript implementation of our listeners:
        //Firing popup and preventing default browser behavior for Ctrl-H
        ctrlHKeyDown = function (event) {
            if ("ctrl H" == event.getKeyStroke().toMarshalledString()) {
              showPopup();     
              event.cancel();              
            }   
        }
        
        //Firing popup
        showPopup = function(){
         //Looking for popup component
         var popup = AdfPage.PAGE.findComponentByAbsoluteId("ctrlHPopup");
         //Define popup alignment 
         var hints = {}; 
         hints[AdfRichPopup.HINT_ALIGN_ID] = 
           AdfPage.PAGE.findComponentByAbsoluteId("it2").getClientId();
         hints[AdfRichPopup.HINT_ALIGN] = AdfRichPopup.ALIGN_AFTER_START;
         //Showing
         popup.show(hints);
        }

        //Hiding popup
        hidePopup = function(){
         var popup = AdfPage.PAGE.findComponentByAbsoluteId("ctrlHPopup");
         popup.hide();
        }    
        
        //If Enter is pressed 
        enterSelection = function(event){         
         if (AdfKeyStroke.ENTER_KEY == event.getKeyStroke().getKeyCode()) {
           setSelectedValue(event);  
         }
        }
        
        //If mouse is clicked
        clickSelection = function(event){         
           setSelectedValue(event);  
        }

        //Submit selected value and hide the popup
        setSelectedValue = function(event) {
            var items = event.getSource();
            var selectedItem = items.getSelectItems()[items.getValue()];
            var inputText = AdfPage.PAGE.findComponentByAbsoluteId("it2");
            var inputContent = AdfDhtmlEditableValuePeer.GetContentNode(inputText);
            inputContent.value = selectedItem.getLabel(); 
            hidePopup();    
        }

And the result of our work looks like this:


That's all. You can download sample application for this post.

2 May 2011

Adding Groovy validation expression programmatically

In this post I'm going to show how you can add Groovy validation rule for an entity's attribute programmatically "on-the-fly". When you build Groovy expression to validate the value of an attribute, you can use predefined keywords "newValue" and "oldValue" referring to the old and new values of the attribute correspondingly.
In the following piece of code we can see how to add Groovy validation expression for some attribute checking that new value of the attribute is not greater than 20:

    //Groovy validation expression
    private static final String VALIDATION_EXPR="newValue <= 20";
    
    //Keyword in the message bundles resource file
    //exprvalue_err=Value must not be greater than {0}   
    private static final String EXPR_VALUE_ERROR="exprvalue_err";    
    private static final int EXPR_MAX_VALUE=20;    
    
    private void addExpressionValidator(AttributeDef at) {
        //creating new validator
        JboExpressionValidator jcv = new JboExpressionValidator(false, VALIDATION_EXPR);
        
        //setting error message
        jcv.setErrorMsgId(EXPR_VALUE_ERROR);
        
        //setting value for the message's token {0}
        HashMap errvaluesMap = new HashMap();
        errvaluesMap.put("0", EXPR_MAX_VALUE);         
        jcv.setErrorMsgExpressions(errvaluesMap);                
        
        //adding validator to the attribute's validators list
        ((AttributeDefImpl) at).addValidator(jcv);
    }


That was really easy!

Working with Oracle SQL object types. Part II

In my previous post I demonstrated how to retrieve and show information from a database table containing an attribute with SQL object type. In this post I'm going to show how to save this information back into the database.
So, I have a table with the following structure:

create table Payment
(ID Number,
 PaymentDate date,
 Currency Varchar2(3),
 Amount Number,
 Contact Tcontact_info
 )

And Tcontact_info SQL object type has the following definition:

create or replace type Tcontact_info as object
(Name Varchar2(100),
 PhoneNumber Varchar2(100),
 CellPhoneNumber Varchar2(100),
 email Varchar2(100)
 )

In order to update data in  Payment table I'm going to use PL/SQL procedure Payment_update:

create or replace procedure Payment_update
(aId Payment.Id%type,
 aPaymentdate Payment.Paymentdate%type,
 aCurrency Payment.Currency%type,
 aAmount Payment.Amount%type,
 aContact Payment.Contact%type
 )
is
begin
  update Payment
    set Paymentdate = aPaymentdate,
        Currency = aCurrency,
        Amount = aAmount,
        Contact = aContact
  where id = aID;      
end;   
 
In my entity object implementation class PaymentImpl I have overridden doDML method:

    protected void doDML(int operation, TransactionEvent e) {
        switch (operation) {
        case DML_UPDATE:
            {
                updateProcedure();
                break;
            }
        case DML_INSERT:
            {
                insertProcedure();
                break;
            }
        case DML_DELETE:
            {
                deleteProcedure();
                break;
            }
        }
    }

Methods insertProcedure(), updateProcedure() and deleteProcedure() are used to call appropriate PL/SQL procedures for corresponding DML modes.
For example, updateProcedure() looks like this:

    private void updateProcedure() {
        DBTransactionImpl dbti = (DBTransactionImpl) getDBTransaction();
        CallableStatement statement =
          dbti.createCallableStatement(("BEGIN " + 
            "Payment_update(:aId, :aPaymentdate, :aCurrency, :aAmount, :aContact);" + 
                                        "END;"), 0);
        try
        {
          statement.setObject("aId",getId());
          statement.setDate("aPaymentdate", getPaymentdate().dateValue());
          statement.setString("aCurrency", getCurrency());  
          statement.setObject("aAmount", getAmount());
          statement.setObject("aContact", getContact());  
          statement.execute();
        }
        catch (SQLException sqlerr)
        {
          throw new JboException(sqlerr);
        }
        finally
        {
          try
          {
            if (statement != null)
            {
              statement.close();
            }
          }
          catch (SQLException closeerr)
          {
            throw new JboException(closeerr);
          }
        }
        
    }
Note, that there is no any magic to pass contact information as a parameter to Callable Statement. PaymentImpl class has usual getter getContact() for its Contact attribute with TcontactInfo class. And TcontactInfo class inherits oracle.jbo.domain.Struct class which is commonly used to work with complex SQL types. That's all.