Reusable and effective JSF List Of Values for huge data sets

Reusable and effective JSF List Of Values for huge data sets

-Professional diary of one team leader-

There are a lot of use cases (regardless of web application development technology), in which you, as developer, need to provide effective, user friendly and fast solution for choosing one of the millions option offered. In case you are using some of server-side web app frameworks (as I am going to use here), special care must be taken in order not to overload the server with a large amount of data, yet providing end user with ability to find and choose data they want to pick.

Technology stack used in this example

I am going to use JavaServer Faces (with PrimeFaces component library), EJB, CDI and JPA. Yes. The good, old JSF. If you wonder why, well… I tell you: The JSF is not what your mom told you he was. I listened to all sorts of nonsense about this great framework. Really. Starting with “reputable technology assessment sites”, to “disappointed” individuals, who “will never, but never, use them” (I will add - mostly of them, never used it properly). But, you know… I do not like others to tailor my opinion. The only principle I stick with is the following: if some technology allows _me to produce fast, stable and modern applications,, I am going to use it. It is completely irrelevant what others write or think about it. At the end, this is all the end user wants, is not? And if you think that JSF application cannot looks ultra modern, think again. Just search Google for : PrimeFaces templates mirage live demo.

Enough on this topic, the point of this article is not to start another pointless technology war.

Inspiration

Over the years working with Oracle ADF framework, a particularly useful concept for working with large data sets, was so called “List Of Values”. Just to get an impression of what we are going to do, take a look at Oracle List Of Value

When used properly, it allows exactly what is the topic of this article – the effective and fast way to pick something from large data set.

What's on offer

PrimeFaces offers a couple of great selection components: SelectOneListBox, SelectOneMenu, and AutoComplete, but when we work with a million offers, we need stronger weapons. Specifically, we'll take the last one, auto complete component, and we will enrich it additional search button, which opens a search dialog. In this dialog, the user has more options for searching , which allows him to more easily locate the data he wants to select. At run-time, on the typical CRUD dialog, it will looks like this (we will not pay much attention to makeup, we will pay attention to the essence and core functionality):

No alt text provided for this image

The component displays suggestions while the input is being type, and you can choose one from that list:

No alt text provided for this image

You can type whatever you want in the input field, if that value not on offer, you will get the error message when submitting form:

No alt text provided for this image

So far, so good; but, all this is just standard behavior of PrimeFaces autoComplete component (well, except JSF validation). The value we are actually adding into the story, is a search button next to the input field, which opens a search and selection dialog in a large data set:

No alt text provided for this image

For that, we will use another PrimeFaces feature: LazyDataModel. This is what allows us to work effectively with a large data set. But more importantly, we will make a JSF component that will be reusable across entire application. To do that, we are going to use JSF composite component

Implementation details

Preparing model: For our sample, we are going to use two DB tables, from Oracle HR demo schema:

No alt text provided for this image

As you can see, there is Foreign key relationship between those tables. When appropriate JPA entities are made on this basis, in the Department @Entity, there is @ManyToOne relationship, pointing to master Location:

No alt text provided for this image

Now, since we want to allow the user to choose a Location by one of the Location's attributes that has meaning for them (so, not by location_id, but by street_address, let say ), we make one modification on the Department entity. We will add one @javax.persistence.Transient attribute in the Department.java entity class:

@Transient
private String locationAddr = "";
public String getLocationAddr() {
   if (getLocation() != null)
     return getLocation().getStreetAddress();
   return locationAddr;
}

public void setLocationAddr(String locationAddr) {
   if (locationAddr == null || locationAddr.isEmpty()) {
    setLocation(null);
   }else {    
    if (getLocation() != null)
        getLocation().setStreetAddress(locationAddr);
    else
       this.locationAddr = locationAddr;
   }
}


As you see, the new LocationAddr attribute returns street_address of the associated Location (if this Department has one). Next, If this attribute is set to a blank value, the Location will effectively revoked. Otherwise, we will just remember the passed value. Note, that this piece of code does not set the selected Location (ie, does not call the setLocation() method). That job will be done by our composite component, by calling the Java method passed to it as an attribute, when end user chooses Location from suggestion, or from Search dialog. We'll get to that a little later.

The point of LocationAddr attribute is that its value serves as a value for the PrimeFace’s autoComplete UI component (the input field left to the search button).

-Next, in the Location.java entity class, lets define a JPA NamedQuery, which will find and return a Locations, based on street_address attribute. We will use that query in to achieve suggestions for PrimeFaces’s autoComplete UI komponent:

@NamedQuery(name="Location.findByAddress", query="SELECT loc FROM Location loc where loc.streetAddress like :addr")

Preparing Stateless EJB: We need one @javax.ejb.Stateless annotated CDI bean, which will obtain the data from the LOCATIONS database table, for PrimeFaces’s LazyDataModel, as well for autoComplete UI komponent (see "Location.findByAddress" JPA NamedQuery, which we have defined above). Also, autoComplete’s JSF Validator will use method from this class, in order to validate Location (to check if Location with some street address really exists). Here is source code:

package service;

import java.util.List;
import java.util.Map;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.primefaces.model.SortOrder;
import model.Location;

@Stateless
public class LocService {
    @PersistenceContext
    private EntityManager entityManager;

        public List<Location> getLocationByAddress(String addr, int firstRes, int maxRes){
            Query query = entityManager.createNamedQuery("Location.findByAddress").
                                                     setFirstResult(firstRes).
                                                              setMaxResults(maxRes);
            query.setParameter("addr", addr + "%");
        List<Location> result = query.getResultList();
        return result;
        }

    public List<Location> getLocations(int first, int pageSize,
                                        String sortField,
                                        SortOrder sortOrder,
                                        Map<String, Object> filters) {
            String sql = "from Location loc where 1=1 ";
            
            Integer locationId =  (Integer)filters.get("locationId");
            String postalCode = (String) filters.get("postalCode");
            String stateProvince = (String)filters.get("stateProvince");
            String streetAddress = (String)filters.get("streetAddress");
            
            if (locationId != null) {
               sql += " and loc.locationId = :locationId ";
            }
            if (postalCode != null) {
               sql += " and loc.postalCode like :postalCode ";
            }        
            if (stateProvince != null) {
               sql += " and loc.stateProvince like :stateProvince";
            }
            if (streetAddress != null) {
               sql += " and loc.streetAddress like :streetAddress";
            }
            
            if (sortField != null) {
               sql += " order by "+sortField+" " +(sortOrder.equals(SortOrder.ASCENDING) ? "ASC" : "DESC");
            }
            
            TypedQuery<Location> query = entityManager.createQuery(sql, Location.class)
                                                                  .setFirstResult(first)
                                                                  .setMaxResults(pageSize);
            
            if (locationId != null) {
               query.setParameter("locationId",  locationId);
            }
            if (postalCode != null) {
               query.setParameter("postalCode",  "%"+ postalCode+"%");
            }
            if (stateProvince != null) {
                   query.setParameter("stateProvince",  "%"+ stateProvince+"%");
            }
            if (streetAddress != null) {
                   query.setParameter("streetAddress",  "%"+ stateProvince+"%");
            }
            
            List<Location> result = query.getResultList();
            
            return result;

    }// of getLocations()
    
    public int getCount(Map<String, Object> filters)  {
        String sql = "select count(loc) from "
                + Location.class.getName() +
                " loc where 1=1 ";
        
        Integer locationId =  (Integer)filters.get("locationId"); 
        String postalCode = (String) filters.get("postalCode");
        String stateProvince = (String)filters.get("stateProvince");
        String streetAddress = (String)filters.get("streetAddress");
        
        if (locationId != null) {
               sql += " and loc.locationId = :locationId ";
        }
        if (postalCode != null) {
           sql += " and loc.postalCode like :postalCode ";
        }        
        if (stateProvince != null) {
           sql += " and loc.stateProvince like :stateProvince";
        }
        if (streetAddress != null) {
           sql += " and loc.streetAddress like :streetAddress";
        }
        
        
        TypedQuery<Long> query = entityManager.createQuery(sql, Long.class);
        if (locationId != null) {
           query.setParameter("locationId",  locationId);
        }
        if (postalCode != null) {
           query.setParameter("postalCode",  "%"+ postalCode+"%");
        }
        if (stateProvince != null) {
               query.setParameter("stateProvince",  "%"+ stateProvince+"%");
        }
        if (streetAddress != null) {
               query.setParameter("streetAddress",  "%"+ stateProvince+"%");
        }

               return query.getSingleResult().intValue();
    }// of getCount()
}

Preparing JSF Validator: This is standard JSF validator, which uses above defined Stateless EJB, in order to check if Location entered in the autoComplete realy exists:

package view.validators;
import java.util.List;

import javax.enterprise.inject.spi.CDI;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.FacesValidator;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

import model.Location;
import service.LocService;

@FacesValidator(value="LocValidator", managed=true)
public class LocValidator implements Validator<String> {
    private LocService locService = CDI.current().select(LocService.class).get();
    @Override
    public void validate(FacesContext fc, UIComponent comp, String locationAddr) throws ValidatorException {
          if (locationAddr == null || locationAddr.isEmpty())
              return;
           List<Location> locs = locService.getLocationByAddress(locationAddr, 0, 1);
            if (locs.size() == 0) {
               throw new ValidatorException( new FacesMessage(FacesMessage.SEVERITY_ERROR,
                                                               "The location " + locationAddr + " does not exist",
                                                              null));
              }
    }// of validate()
}

The Composite Component

That is how we achieve reusabillity. First, we need to prepare so-called ‘backing component’. This is a Java class, annotated with @javax.faces.component.FacesComponent, and extends the javax.faces.component.UINamingContainer. This is a central point, a heart of our List Of Values component. It implements all logic, which combines the concepts we used here into one functional unit. We will point to the methods of this class, in the composite component’s facelet source code. The FacesComponent value attribute, we will use as the composite component’s componentType . Here is source code, and I am going to explain most important methods from this class:

package cc_types;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.el.ELContext;
import javax.el.MethodExpression;
import javax.enterprise.inject.spi.CDI;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import org.primefaces.PrimeFaces;
import org.primefaces.component.dialog.Dialog;
import org.primefaces.event.CloseEvent;
import org.primefaces.event.SelectEvent;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortOrder;
import model.Location;
import service.LocService;

@FacesComponent(value="loc_LOVCCType")
public class Loc_LOVCCType extends UINamingContainer {
       private LocService locService = CDI.current().select(LocService.class).get();
    
       public List<String> completeLoc(String query) {
           List<Location> locs = locService.getLocationByAddress(query, 0, 10);  
           List<String> result = new ArrayList<String>();
           for (Location loc: locs) {
               result.add(loc.getStreetAddress());
           }
           return result;
       }
        
       private void invokeLocChoosenListener(Object lChoosenObj, Location choosenLoc) {
           if (lChoosenObj != null && lChoosenObj instanceof MethodExpression) {
               ELContext elContext = FacesContext.getCurrentInstance().getELContext();
               MethodExpression me = (MethodExpression)lChoosenObj;
               me.invoke(elContext, new Object[] {choosenLoc});
           }
       }
       
       public void handleLocSelect(SelectEvent event) {
           if (event != null && event.getObject() != null) {
               Object selectedObject = event.getObject();
               if (selectedObject != null && selectedObject instanceof String) {
                   List<Location> locs = locService.getLocationByAddress((String)selectedObject, 0, 1);
                   if (locs.size() == 1) {
                       Location choosenLoc = (Location)locs.get(0);
                       Object lChoosenObj = getAttributes().get("locChoosenListener");
                       invokeLocChoosenListener(lChoosenObj, choosenLoc);
                   }
               }
           }
       }// of handleLocSelect()
       
       public void handleTblRowSelect(SelectEvent event) {
           Location selLoc = (Location)event.getObject();
           Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
           viewMap.put(getClientId() + "_lovSrchDlg_selectedRow", selLoc);
       }
       
       public void setLocLazyModel(LazyDataModel<Location> value) {
           Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
           viewMap.put(getClientId(), value);   
       }
       
       public LazyDataModel<Location> getLocLazyModel() {
           Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
                   // if the model already in the viewScope, return him:
           Object lazyModelObj = viewMap.get(getClientId() + "_lovSrchDlg_");
           if (lazyModelObj != null && lazyModelObj instanceof LazyDataModel) {
               return (LazyDataModel)lazyModelObj;
           }
           // othervise, make one, adn put in the viewScope, under attribute clientId + "_lovSrchDlg_"
           LazyDataModel locLazyModel = new LazyDataModel<Location> (){
               public List<Location> load(int first, int pageSize,
                                                String sortField,
                                                SortOrder sortOrder, Map<String, Object> filters) {
                  List<Location> list = locService. getLocations(first, pageSize, sortField, sortOrder, filters);
                        setRowCount(locService.getCount(filters));
                        return list;
                   }// of load() method
                   
                public Location getRowData(String rowKey) {
                   long locId = (new Long(rowKey)).longValue();
                   List<Location> locs = (List<Location>) getWrappedData();
                   for(Location loc : locs) {
                       if(loc.getLocationId() == locId)
                           return loc;
                   }
                   return null;
               }// of getRowData
                   
               public Object getRowKey(Location loc) {
                   return loc.getLocationId();
               }
             };// of inline impl
               
               viewMap.put(getClientId() + "_lovSrchDlg_", locLazyModel);
                  return locLazyModel;
        }// of getLocLazyModel()
       
       private Location getSelectedRow() {
           Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
           return (Location)viewMap.get(getClientId() + "_lovSrchDlg_selectedRow");
       }
        
        public void onChooseCancel(ActionEvent e) {
            String btnId =  e.getComponent().getId();
            Location locToReturn = ("cancelBtn".equalsIgnoreCase(btnId) ? null : getSelectedRow());
            
            if (locToReturn != null) {
                   Object lChoosenObj = getAttributes().get("locChoosenListener");
                   invokeLocChoosenListener(lChoosenObj, locToReturn);
                   PrimeFaces.current().ajax().update(findComponent("lovValue").getClientId());
            }
                        UIComponent comp = findComponent("srchDlg");
                     if (comp != null && comp instanceof Dialog) {
                    Dialog srchDlg = (Dialog)comp;
               String jsCloseDlg = "PF('" + srchDlg.resolveWidgetVar() + "').hide()";
                    PrimeFaces.current().executeScript(jsCloseDlg);
                    }    
        } // of onChooseCancel()

        public void handleLovDlgClose(CloseEvent event) {
            cleanup();
        }
        
        private void cleanup() {
           Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
           viewMap.remove(getClientId() + "_lovSrchDlg_");
           viewMap.remove(getClientId() + "_lovSrchDlg_selectedRow");
           org.primefaces.component.datatable.DataTable locTbl = (org.primefaces.component.datatable.DataTable)findComponent("srchDlgForm:locTbl");
           locTbl.resetValue();
        }
        public void openLovSrchDlg(ActionEvent e) {
           UIComponent comp = findComponent("srchDlg");
           if (comp != null && comp instanceof Dialog) {
               Dialog srchDlg = (Dialog)comp;
               String jsShowDlg = "PF('" + srchDlg.resolveWidgetVar() + "').show();";
               PrimeFaces.current().executeScript(jsShowDlg);
               PrimeFaces.current().ajax().update(srchDlg.getClientId());
           }
        }
}

Lets explain important methods from this class. There are two ways to choose some Location:

 - choose one from Suggestion list:

No alt text provided for this image

In that case, we need a way to react to this user action (in order to set chosen Location into Department entity), and we can achieve this by reacting on the PrimeFaces’s "itemSelect" ajax event, for autoComplete UI component. The

public void handleLocSelect(SelectEvent event)

is a listener for that event. The SelectEvent param carries information about chosen String from Suggestion list, we can take that info , and pass it on stateless EJB service, in order to locate the Location entity with that street_address attribute. If such a Location is found, it needs to be effectively set up as Location in Department JPA entity. The important part: in the composite component, there is one attribute, with name="locChoosenListener", representing a Java method passes as parameter to c. component, with a parameter of Location type (we'll see him a little later, in the c. component facelet's source code). So we can invoke that method with help of MethodExpression, and pass founded Location as parameter. In fact, our component does not know what method exactly does, her job is just to call this method, and pass founded Location as param. For details, see

private void invokeLocChoosenListener(Object lChoosenObj, Location choosenLoc)

Of course, the method we will pass as component attribute, will set chosen Location in Department entity, by calling setLocation() method from Department.java entity.

-The second way to choose some Location, is by selecting table row, and press Choose button on the component’s Search dialog:

No alt text provided for this image

In that case, the actionListener for the Choose button,

public void onChooseCancel(ActionEvent e)

will call the invokeLocChoosenListener() again.

Note: the most natural way to inform component’s caller about chosen Location, is to use CDI event notification model (ie, Observer pattern). Then why I choose to pass and invoke method via MethodExpression API? Because the caller of composite component can be another one composite component. And at time this writing, the JSF (2.3) does not support the @javax.enterprise.event.Observes annotation in the @FacesComponent Java class. That will not work.

Next important question to solve, is following: how to enable the RequestScope lifetime JSF artefact (like a @FacesComponent annotated Java class is), to implement and deal with PrimeFaces LazyDataModel, which is by nature much closer to the ViewScope lifetime? That is what

public LazyDataModel<Location> getLocLazyModel()

method solves. This method provides a value for dataTable used in Search dialog. The trick I use here, is to build Lazy model instance, and put him in the ViewScope, under attribute consisting of clientId + "_lovSrchDlg_" (if it does not already exist there). The next time the method is called, it will return a value from that ViewScope attribute. This concept allows for multiple instances of composite component to keep their LazyDataModel instances in different, unique ViewScope attributes. Similar does the

public void handleTblRowSelect(SelectEvent event)

method,which is the listener for the "rowSelect" ajax event, attached to the dataTable, in order to keep currently selected Location in the ViewScope.

In the end, one more thing remained: the composite component .xhtml facelet source code. As usual in JSF, we need to place composite component .xhtml code in the /resources folder of our WebContent. So, /resources folder should be at the same level as /WEB-INF folder in the web application structure. We will put c.component source code in the separate sub folder, let say, /resources/lovs, so the ‘lovs’ will be the namespace at the point (I mean, JSF .xhtml facelet file) where we will use our component. Lets name our composite componet facelet file as loc_LOV_cc.xhtml :

No alt text provided for this image

Our composite component need a couple of things:

  • one attribute, through which the component is passed a String value representing Location (our new LocationAddr entity attribute value, from the Department EJB entity)
  • one PrimeFaces autoComplete UI component, it is where the user enters location’s street_address (actually, our new LocationAddr transient attribute value)
  • one commandButton, by which the user opens a dialog for additional search and scroll through large data set
  • one dialog, showing the data table with Locations

Beside this, we also need c. component attribute, representing a Java method, which will effectively set the selected Location in the Department entity (when end user choses one). The already mentioned invokeLocChoosenListener() will cal him. Here is loc_LOV_cc.xhtml facelet source code:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="https://www.linkedin.com/redir/general-malware-page?url=http%3A%2F%2Fjava%2esun%2ecom%2Fjsf%2Ffacelets"
      xmlns:f="https://www.linkedin.com/redir/general-malware-page?url=http%3A%2F%2Fjava%2esun%2ecom%2Fjsf%2Fcore"
      xmlns:h="https://www.linkedin.com/redir/general-malware-page?url=http%3A%2F%2Fjava%2esun%2ecom%2Fjsf%2Fhtml"
      xmlns:p="http://primefaces.org/ui"
      xmlns:composite="https://www.linkedin.com/redir/general-malware-page?url=http%3A%2F%2Fjava%2esun%2ecom%2Fjsf%2Fcomposite">
      
    <composite:interface componentType="loc_LOVCCType">
         <composite:attribute name="locationName" required="true"/>
         <composite:attribute name="locChoosenListener" method-signature="void observeLocationChoose(model.Location)" required="true"/>
    </composite:interface>
    
    <composite:implementation>
     <div id="#{cc.clientId}">
        <p:panelGrid id="locData" columns="3" >
                 <p:autoComplete id="lovValue" value="#{cc.attrs.locationName}"
                                 completeMethod="#{cc.completeLoc}"  validator="LocValidator">
                        <p:ajax event="itemSelect" listener="#{cc.handleLocSelect}"/>
                        <p:ajax process="@this" update="@this, locMsg"/>                                 
         </p:autoComplete>
         <p:commandButton immediate="true" icon="ui-icon-search" process="@this" actionListener="#{cc.openLovSrchDlg}" />
         <p:message for="lovValue" display="icon" id="locMsg"/>
        </p:panelGrid>
               
        <p:dialog  id="srchDlg" dynamic="true"
                   appendTo="@(body)" modal="true"
                   header="Search and select: Location"
                   width="650" height="350">
                   <p:ajax event="close" listener="#{cc.handleLovDlgClose}"/>
                   <h:form id="srchDlgForm">
                     <p:dataTable  id="locTbl" var="loc"
                                   value="#{cc.locLazyModel}"
                       rowIndexVar="locRowIndex"
                       selectionMode="single"
                       rowKey="#{loc.locationId}"
                       scrollRows="20" rows="40" scrollable="true" virtualScroll="true" scrollHeight="200" lazy="true">
                       <p:ajax event="rowSelect" listener="#{cc.handleTblRowSelect}"/>
                       <p:column headerText="Location Id">
                          <h:outputText value="#{loc.locationId}"/>
                       </p:column>
                            
                       <p:column headerText="Postal Code" filterBy="#{loc.postalCode}" filterMatchMode="contains">
                          <h:outputText value="#{loc.postalCode}"/>
                       </p:column>
                            
                      <p:column headerText="Street Address" filterBy="#{loc.streetAddress}" filterMatchMode="contains">
                         <h:outputText value="#{loc.streetAddress}"/>
                      </p:column>
              </p:dataTable>     
           </h:form>    

                   <f:facet name="footer">
                     <p:toolbar >
                            <f:facet name="left">
                         <p:commandButton id="chooseBtn" value="Choose" icon="ui-icon-disk" actionListener="#{cc.onChooseCancel}" process="@this" />
                         <p:commandButton id="cancelBtn" value="Cancel" icon="ui-icon-cancel" immediate="true" actionListener="#{cc.onChooseCancel}" process="@this" />
                            </f:facet>
                 </p:toolbar>
                   </f:facet>                                
        </p:dialog>       
      </div>
    </composite:implementation>
</html>    
     

Pay attention on <p:ajax event="close" listener="#{cc.handleLovDlgClose}"/>

attached to <p:dialog>. That listener will ensure resources cleanup (removing LazyDataModel instance from ViewScope when the Search dialog closes). Now everything is ready, and the component can be used

Using Composite Component

The first thing to do, in order to use our composite component in some .xhtml facelet page, is to add appropriate namespace into the <html> tag. Now, this article editor does not allow me write namespace, so I will point on the link, where you can see the form you should use

On that page, just search for:

"Components from that taglibrary may be used in a using page by declaring them in the XML namespace for that view"


Now, on the place where we need it, we can add following:

<lovs:loc_LOV_cc id="lLovcc"
     locationName="#{homeBean.deptData.locationAddr}"
     locChoosenListener="#{homeBean.setLocationInDept}">
</lovs:loc_LOV_cc>

On above sample, the homeBean is a ViewScoped CDI bean, with ‘deptData’ property (public getter/setter pair) of Department type - one that is being edited on the UI page. Also, there is Java method that is being sent to the c. component's "locChoosenListener" attribute, in order to actually set chosen Location into edited Department entity, with just one line of code:

 public void setLocationInDept(Location choosenLoc) {
   getDeptData().setLocation(choosenLoc);
 }

This way, we got a reusable JSF component, which works with large data sets with ease, yet quite simple to use. And… well, that is all ;-),

-Best regards,...

Eli Pirian

Creative Director at Baciano

3y

These are awesome 🔥

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics