1 2 Previous Next 23 Replies Latest reply on Aug 31, 2009 1:29 AM by kragoth Go to original post
      • 15. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
        vladimir.kovalyuk

        Francisco Peredo wrote:



        The funny thing is that the Seam team felt that the standard XML declarative navigation of JSF was insufficient and created they own "extended not standard XML declarative navigation". Wouldn't it be better if Seam offered a programmatic API for navigation that could be extended to support XML declarative navigation if and only if an application demands requires it?


        Exactly, I believe declarative navigation and conversation lifecycle control, as well as integration with JBPM, all those should have been built on top of core classes that provide API and extension points.


        Now to complete a simple task - to start a natural conversation programmatically - I have to investigate what methods are called in declarative approach and then I have to mimic the sequence of invocations (methods of different components!). Needless to say about maintainability of that sort of code.


        • 16. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
          kragoth

          Hey guys,


          I'm going to post my Navigation code the way it is in my application at the moment. Which means it will not plug into anyone else's code base. But, maybe it will help someone get an idea of how to implement a typesafe and fully java based navigation system. I have been meaning to strip it down to allow straight copy and paste into any app but.... time stops for no one :P


          So Francisco, I hope you enjoy having a look at this :)



          This class is my core class to my navigation system. I have lots of methods that don't need to exist, but are there due to some of the unusual navigation that takes place in our app. This code has NOT been cleaned up and thus there is probably a lot of things that could be done better/cleaner.


          package gekko.web.navigation;
          
          import ....* (save space :P)
          
          
          /**
           * When using navigation manager it is important to know a limitation that exists.
           * If you have a SEAM bean that is instantiated because of an action on a xhtml
           * page, and the method called performs a module redirect. 
           * The SEAM bean that was instantiated will be in the conversation belonging to the
           * new module.
           * A simple workaround for this is to ensure you @In the particular SEAM bean into
           * a bean that you know will have been instantiated prior to the action being called.
           *
           */
              
          @Name(NavigationManager.SEAM_ID)
          @Scope(ScopeType.SESSION)
          @Install
          @Startup //This will start up an instance of this class when a session is created
          public class NavigationManager extends AbstractSeamComponent {
              private static final Logger log = LogManager
                  .getLogger(NavigationManager.class);
          
              public static final String SEAM_ID = "NavigationManager";
              public static final String NO_BREADCRUMB_ID = "none";
              
          //
              @In(value = CertificateSessionBean.SEAM_ID, create = true)
              private CertificateSessionBean certificateSessionBean;
              
              
              private final List<String> allConversations = new ArrayList<String>();
              private final Stack<String> conversationStack = new Stack<String>();
              private final Map<String, Breadcrumb> breadcrumbs = 
                  new HashMap<String, Breadcrumb>();
              private final Map<String, Boolean> dirtyStatus =
                  new HashMap<String, Boolean>();
              private Boolean globalNavigationLocked = Boolean.FALSE;
              private String navigationLockedBy = null;
          
          
              public static NavigationManager instance() {
                  return 
                      (NavigationManager) 
                          Component.getInstance(NavigationManager.SEAM_ID);
              }
          
              public String getCurrentConversationId() {
                  return conversationStack.peek();
              }
              
              public boolean isCurrentConversationDirty() {
                  return dirtyStatus.get(getCurrentConversationId());
              }
              
              public void setCurrentConversationDirty(boolean dirty) {
                  dirtyStatus.put(getCurrentConversationId(), dirty);
              }
              
              public String getParentConversationId(String childConversationId) {
                  int parentIndex = conversationStack.indexOf(childConversationId)-1;
                  if (parentIndex < 0) {
                      return null;
                  }
                  return conversationStack.get(parentIndex);        
              }
              
              private void addConverstion(String conversationId) {
                  allConversations.add(conversationId);
                  dirtyStatus.put(conversationId, false);
              }
              
              private void removeConversation(String conversationId) {
                  if (log.isTraceEnabled()) {
                      log.trace("Destroying conversation: " + conversationId);
                  }
                  switchConversation(conversationId);
                  Conversation.instance().end();
                  Conversation.instance().leave();
                  ConversationEntries.instance()
                      .removeConversationEntry(conversationId);
                  breadcrumbs.remove(conversationId);
                  allConversations.remove(conversationId);
                  dirtyStatus.remove(conversationId);
                  conversationStack.remove(conversationId);
              }
          
              /**
               * This method should be called by buttons which reset the dirty flag.
               * "Save" and "Reset" buttons are good examples of this. 
               */
              public void clearDirtyFlagForConversation(String conversationId) {
                  Validate.isTrue(!StringUtils.isBlank(conversationId), "ConversationId cannot be null");
                  dirtyStatus.put(conversationId, false);
              }
              
              public void clearCurrentConversationDirtyFlag() {
                  clearDirtyFlagForConversation(Conversation.instance().getId());
              }
          
              public void removeAllNestedConversations(String conversationId) {
          
                  int stackPosition = conversationStack.indexOf(conversationId);
          
                  // Brand new controller there will be no nested conversations.
                  if (stackPosition < 0) {
                      return;
                  }
          
                  List<String> removePortion = new ArrayList<String>( 
                      conversationStack.subList(stackPosition+1, conversationStack.size()));
          
                  for (int i = removePortion.size()-1; i >= 0; i--) {
                      removeConversation(removePortion.get(i));
                  }
          
                  switchConversation(conversationStack.peek());
          
              }
          
              private void ensureNoErrorMessagesToDisplay() {
                  for (FacesMessage message : FacesMessages.instance()
                      .getCurrentMessages())
                  {
                      if (message.getSeverity().getOrdinal() > FacesMessage.SEVERITY_WARN.getOrdinal()) {
                          throw new IllegalStateException(
                              "An error messages was generated but " +
                              "would be lost if redirect occured. " +
                              "The message was: " + message.getDetail());
                      }
                  }
              }
          
              public <T extends AbstractSeamComponent> T createController(
                  Class<T> clazz)
              {
                  Validate.notNull(clazz, "Class parameter cannot be null");
                  
                  if (!Conversation.instance().isLongRunning()) {
                      throw new IllegalStateException(
                          "Navigation Manager cannot be used outside of a "
                              + "long running conversation.");
                  }
          
                  Validate.isTrue(isConversationOrSessionScopedBean(clazz),
                      "NavigationManager.createController can only be used on "
                          + "Conversation or Session Scoped beans");
          
                  String currentConversationId = Conversation.instance().getId();
          
                  if (log.isTraceEnabled()) {
                      log.trace("Creating controller of type: " + clazz.getCanonicalName()
                      + " called from conversation: " + currentConversationId);
                  }
          
                  Conversation.instance().leave();
          
                  T controller = clazz.cast(Component.getInstance(clazz,
                      getScopeType(clazz), true));
                  Conversation.instance().begin();
                  
                  //Force the bijection interceptor to fire.
                  if (controller instanceof AbstractSeamComponent) {
                      ((AbstractSeamComponent)controller).gekkoInit();
                  }
                  
                  String newConversationId = Conversation.instance().getId();
                  addConverstion(newConversationId);
                  
                  if (log.isTraceEnabled()) {
                      log.trace("Controller was started in conversation: "
                      + newConversationId);
                  }
                  Manager.instance().switchConversation(currentConversationId);
                  if (log.isTraceEnabled()) {
                      log.trace("After component created Switched back to conversation: "
                      + Conversation.instance().getId());
                  }
          
                  
                  return controller;
              }
          
              private <T> ScopeType getScopeType(Class<T> clazz) {
                  return clazz.getAnnotation(Scope.class).value();
              }
          
              private boolean isSessionScopedBean(Class<?> clazz) {
                  return clazz.isAnnotationPresent(Scope.class)
                      && ScopeType.SESSION.equals(getScopeType(clazz));
              }
          
              private boolean isConversationScopedBean(Class<?> clazz) {
                  return clazz.isAnnotationPresent(Scope.class)
                      && ScopeType.CONVERSATION.equals(getScopeType(clazz));
              }
          
              private boolean isConversationOrSessionScopedBean(Class<?> clazz) {
                  return isSessionScopedBean(clazz) || isConversationScopedBean(clazz);
              }
          
              public void lockGlobalNavigation() {
                  // No need to re-lock.
                  if (!globalNavigationLocked) {
                      globalNavigationLocked = Boolean.TRUE;
                      navigationLockedBy = getCurrentConversationId();
                  }
              }
          
              public void unlockGlobalNavigation() {
                  // Only allow the locking controller to unlock and only if it is the
                  // current controller
                  if (getCurrentConversationId().equals(navigationLockedBy)) {
                      globalNavigationLocked = Boolean.FALSE;
                      navigationLockedBy = null;
                  }
              }
          
              private void unlockGlobalNavigationCauseByRedirect(Object conversationId) {
                  if (!conversationStack.contains(navigationLockedBy) ||
                      (conversationStack.contains(conversationId) && 
                          conversationStack.indexOf(conversationId) < 
                          conversationStack.indexOf(navigationLockedBy)))
                  {
                      globalNavigationLocked = Boolean.FALSE;
                      navigationLockedBy = null;
                  }
          
                  if (conversationStack.isEmpty()) {
                      globalNavigationLocked = Boolean.FALSE;
                      navigationLockedBy = null;
                      return;
                  }
              }
          
              public boolean isGlobalNavigationLocked() {
                  return globalNavigationLocked;
              }
          
          
              public void gotoHome() {
                  String conversationId;
                  Breadcrumb breadcrumb;
          
                  if (conversationStack.isEmpty()) {
                      Conversation.instance().leave();
                      Conversation.instance().begin();
                      conversationId = Conversation.instance().getId();
                      Component.getInstance(CertificateMasterController.class, ScopeType.CONVERSATION, true);
                      breadcrumb = new Breadcrumb(conversationId);
                      conversationStack.push(conversationId);
                      breadcrumbs.put(conversationId, breadcrumb);
                  }
          
                  conversationId = conversationStack.firstElement();
                  breadcrumb = breadcrumbs.get(conversationId);
                  Validate.notNull(conversationId, "error getting conversationId");
                  Validate.notNull(breadcrumb, "error getting breadcrumb");
          
                  if (allConversations.isEmpty()) {
                      addConverstion(conversationId);
                  }
          
                  breadcrumb.setConversationId(conversationId);
                  breadcrumb.setModuleBreadcrumbId(MainMenuController.BREADCRUMB_ID);
                  breadcrumb.setModuleBreadcrumbParams(null);
                  breadcrumb.setModuleViewId(AssignedCertificatesActionBean.VIEW_ID);
                  breadcrumb.setModuleNavigation(
                      new AbstractSimpleForwardBackNavigation<CertificateMasterController, MainMenuController>(conversationId) {
          
                          @Override
                          protected void back(MainMenuController backController) {
                              backController.gotoHome();
                          }
          
                          @Override
                          public void navigateForward(
                              CertificateMasterController controller)
                          {
                              controller.displayAssignedCertificates();
                          }
          
                      });
                  breadcrumb.setPageBreadcrumbId(null);
                  breadcrumb.setPageBreadcrumbParams(null);
                  breadcrumb.setPageViewId(null);
          
                  switchConversation(conversationId);
                  CertificateMasterController certMasterController =
                      (CertificateMasterController) Component.getInstance(CertificateMasterController.class, ScopeType.CONVERSATION, false);
          
                  certificateSessionBean.refreshCertificateLists();
                  certMasterController.gotoAssignedCertificates();
              }
          
              public void rootLevelRedirect(String viewId) {
                  final String rootConversationId = conversationStack.firstElement();
                  // TODO: removeAllNested is redundant because implementRedirect
                  removeAllNestedConversations(rootConversationId);
                  implementRedirect(
                      rootConversationId, 
                      viewId, 
                      breadcrumbs.get(rootConversationId));
              }
              
              public void reset() {
                  String currentViewId = FacesContext.getCurrentInstance()
                      .getViewRoot().getViewId();
                  final String currentConversationId = getCurrentConversationId();
                  implementRedirect(
                      currentConversationId, 
                      currentViewId, 
                      breadcrumbs.get(currentConversationId));
              }
             
              public Map<String, Breadcrumb> getBreadcrumbs() {
                  return breadcrumbs;
              }
          
              public List<Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean>>
                  getCurrentBreadcrumbs()
              {
                  List<Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean>> breadcrumbDisplaybeans =
                      new ArrayList<Pair<BreadcrumbDisplayBean,BreadcrumbDisplayBean>>();
                  
                  for (int i = 0; i < conversationStack.size(); i++) {
                      Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean> displayBeans =
                          constructBreadcrumbDisplayBeansForConversation(conversationStack.get(i));
                      
                      if (null != displayBeans) {
                          breadcrumbDisplaybeans.add(displayBeans);
                      }
                  }
                  return breadcrumbDisplaybeans;
              }
              
              private Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean> 
              constructBreadcrumbDisplayBeansForConversation(
                  String conversation)
              {
                  Breadcrumb breadcrumb = breadcrumbs.get(conversation);
                  if (null == breadcrumb) {
                      return new Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean>(null, null);
                  }
                  
                  String moduleCrumbName = 
                      getBreadcrumbForKey(breadcrumb.getModuleBreadcrumbId(), 
                          breadcrumb.getModuleBreadcrumbParams());
                  
                  BreadcrumbDisplayBean moduleDisplayBean = 
                      new BreadcrumbDisplayBean(
                          moduleCrumbName,
                          breadcrumb.getModuleNavigation());
                  
                  Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean> breadcrumbPair =
                      new Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean>(
                          moduleDisplayBean, null);
                  
                  if (breadcrumb.getPageBreadcrumbId() != null) {
                      String pageCrumbName = 
                          getBreadcrumbForKey(breadcrumb.getPageBreadcrumbId(), 
                              breadcrumb.getPageBreadcrumbParams());
                  
                      BreadcrumbDisplayBean pageDisplayBean = 
                          new BreadcrumbDisplayBean(
                              pageCrumbName,
                              breadcrumb.getPageNavigation());
                      
                      breadcrumbPair =
                          new Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean>(
                              moduleDisplayBean, pageDisplayBean);
                  }
                  
                  return breadcrumbPair;
              }
              
              public Pair<BreadcrumbDisplayBean, BreadcrumbDisplayBean> 
              getBreadcrumbDisplayBeansForConversation(String conversation) {
                  return constructBreadcrumbDisplayBeansForConversation(conversation);
              }
              
              public void breadcrumbNavigate(String conversationId, String viewId) {
                  Breadcrumb breadcrumb = breadcrumbs.get(conversationId);
                  redirectToPage(viewId, 
                      conversationId, 
                      breadcrumb.getPageBreadcrumbId(), 
                      breadcrumb.getPageBreadcrumbParams());
              }
              
              //This will eventually be superceeded.
              public String getBreadcrumbTrail() {
          
                  StringBuilder builder = new StringBuilder("");
                  for (int i = 0; i < conversationStack.size(); i++) {
                      Breadcrumb currentCrumb = breadcrumbs.get(conversationStack.get(i));
                      builder.append(
                          getBreadcrumbForKey(currentCrumb.getModuleBreadcrumbId(), 
                              currentCrumb.getModuleBreadcrumbParams()));
                      
                      if (!StringUtils.isBlank(currentCrumb.getPageBreadcrumbId())) {
                          builder.append(" > ");
                          builder.append(
                              getBreadcrumbForKey(currentCrumb.getPageBreadcrumbId(),
                                  currentCrumb.getPageBreadcrumbParams()));
                      }
                      
                      if (i < (conversationStack.size() -1)) {
                          builder.append(" > ");
                      }
                  }
                  
                  return builder.toString();
              }
              
              public String getBreadcrumbForKey(String key, Object... breadcrumbParams) {
                  if (ResourceBundle.instance().containsKey(key)) {
                      return MessageUtils.getResourceMessage("bundles.breadcrumb_messages", key, breadcrumbParams);
                  } else {
                      return key;
                  }
              }
              
              public String getPageBreadcrumbIdForConversation(String conversationId) {
                  Breadcrumb bc = getBreadcrumbForConversation(conversationId);
                  
                  if(bc == null) {
                      return null;
                  }
                  
                  return bc.getPageBreadcrumbId();
              }
              
              public String getCurrentViewId() {
                  Breadcrumb bc = getCurrentBreadcrumb();
                  
                  if(bc == null) {
                      return null;
                  }
                  
                  return bc.getPageViewId();
              }
              
              public Breadcrumb getCurrentBreadcrumb() {
                  return getBreadcrumbForConversation(getCurrentConversationId());
              }
              
              public Breadcrumb getBreadcrumbForConversation(String conversationId) {
                  if(breadcrumbs.containsKey(conversationId)) {
                      return breadcrumbs.get(conversationId);
                  }
                  
                  return null;
              }
              
              /**
               * <b>Inter-module navigation method.</b>
               * This is the basic method to call when navigating from one module to
               * another. No params can be supplied to the breadcrumbs.
               * 
               * 
               * @param conversationId the conversation to go to
               * @param viewId the .xhtml page to go to
               * @param pageBreadcrumbId should be left null if you don't 
               * want it to show
               */
              public void redirectToModule(
                  final String conversationId, 
                  final String viewId, 
                  final String moduleBreadcrumbId, 
                  final String pageBreadcrumbId)
              {
                  Validate.notNull(conversationId, "conversationId cannot be null");
                  Validate.notNull(viewId, "viewId cannot be null");
                  Validate.notNull(moduleBreadcrumbId, "moduleBreadcrumbId cannot be null");
                  
                  Breadcrumb breadcrumb = breadcrumbs.get(conversationId);
                  
                  if (null == breadcrumb) {
                      breadcrumb = new Breadcrumb(conversationId);
                      breadcrumbs.put(conversationId, breadcrumb);
                  }
                  breadcrumb.setPageBreadcrumbParams(null);
                  breadcrumb.setPageViewId(viewId);
                  breadcrumb.setPageBreadcrumbId(pageBreadcrumbId);
                  breadcrumb.setModuleBreadcrumbId(moduleBreadcrumbId);
                  breadcrumb.setModuleViewId(viewId);
                  breadcrumb.setModuleBreadcrumbParams(null);
                  //TODO fix this. Not really right.
                  breadcrumb.setModuleNavigation(
                      new BackNavigation() {
                          private final NavigationManager navMan =
                              NavigationUtils.getSeamBean(NavigationManager.class);
                          @Override
                          public void navigateBack() {
                              navMan.implementRedirect(
                                  conversationId,
                                  viewId,
                                  NavigationManager.this.getBreadcrumbs()
                                      .get(conversationId));
                          }
                      });
                  
                  implementRedirect(conversationId, viewId, breadcrumb);
              }
              
              /**
               * General redirect method. Should be used only when the other redirect
               * methods are not adequate.
               * 
               */
              public void implementRedirect(
                  String conversationId, 
                  String viewId, 
                  final Breadcrumb breadcrumb)
              {
                  Validate.notNull(viewId, "param viewId cannot be null");
                  Validate.notNull(conversationId, "param controller cannot be null");
                  Validate.notNull(breadcrumb, "param breadcrumb cannot be null");
                  Validate.isTrue(allConversations.contains(conversationId),
                      "Navigation Manager does not know about the conversation with id: "
                          + conversationId);
                  
                  ensureNoErrorMessagesToDisplay();
                  unlockGlobalNavigationCauseByRedirect(conversationId);
                  removeAllNestedConversations(conversationId);
                  switchConversation(conversationId);
                  
                  //Generate the navActions if not provided.
                  //So that the breadcrumbs are clickable.
                  if (null == breadcrumb.getModuleNavigation()) {
                      breadcrumb.setModuleNavigation(
                          getGeneratedBreadcrumbNavAction(
                              breadcrumb, breadcrumb.getModuleViewId()));
                  }
                  //Only do the page breadcrumb if needed
                  if (!StringUtils.isBlank(breadcrumb.getPageBreadcrumbId()))
                  {
                      breadcrumb.setPageNavigation(
                          getGeneratedBreadcrumbNavAction(
                              breadcrumb, breadcrumb.getPageViewId()));
                  }
                  
                  breadcrumbs.put(conversationId, breadcrumb);
                  
                  Manager.instance().redirect(viewId, conversationId);
                  if (!conversationStack.contains(conversationId)) {
                      conversationStack.add(conversationId);
                  }
              }
          
              private BackNavigation getGeneratedBreadcrumbNavAction(
                  final Breadcrumb breadcrumb, final String viewId)
              {
                  return new BackNavigation() {
                      private final NavigationManager navMan =
                          NavigationUtils.getSeamBean(NavigationManager.class);
                      
                      @Override
                      public void navigateBack() {
                          navMan.implementRedirect(
                              breadcrumb.getConversationId(),
                              viewId,
                              breadcrumb);
                      }
                  };
              }
              
              public boolean conversationExists(String conversationId) {
                  return allConversations.contains(conversationId);
              }
              
              /**
               * <b>Intra-module navigation method</b>
               * This method is for navigating between pages belonging to the
               * current module.
               * 
               */
              public void redirectToPage(
                  String converastionId,
                  String viewId,
                  String pageBreadcrumbId,
                  Object... params)
              {
                  Validate.notNull(converastionId, "conversationId cannot be null");
                  Validate.notNull(viewId, "viewId cannot be null");
                  
                  Breadcrumb breadcrumb = breadcrumbs.get(converastionId);
                  Validate.notNull(breadcrumb, 
                      "No breadcrumb was found. " +
                      "Maybe you should be using redirectToModule instead of " +
                      "redirect to page.");
                  
                  breadcrumb.setPageViewId(viewId);
                  breadcrumb.setPageBreadcrumbId(pageBreadcrumbId);
                  breadcrumb.setPageBreadcrumbParams(params);
                  
                  implementRedirect(converastionId, viewId, breadcrumb);
              }
              
              public void redirectToConverstaion(String conversationId) {
                  final Breadcrumb breadcrumb = breadcrumbs.get(conversationId);
                  final String backPageViewId = breadcrumb.getPageViewId();
                  final String backModuleViewId = breadcrumb.getModuleViewId();
                  if (null == backPageViewId) {
                      implementRedirect(
                          conversationId, 
                          backModuleViewId,
                          breadcrumb);
                  } else {
                      implementRedirect(
                          conversationId,
                          backPageViewId,
                          breadcrumb);
                  }
              }
              
              public String createConversation() {
                  String currentConversationId = Conversation.instance().getId();
                  Conversation.instance().leave();
                  String newConversationId = Conversation.instance().getId();
                  Validate.isTrue(!currentConversationId.equals(newConversationId), 
                      "A new conversation was not created.");
                  Conversation.instance().begin();
                  
                  addConverstion(newConversationId);
                  switchConversation(currentConversationId);
                  
                  return newConversationId;
              }
              
              public <T extends AbstractSeamComponent> void 
                  createController(String conversationId, Class<T> clazz)
              {
                  String currentConversation = Conversation.instance().getId();
                  switchConversation(conversationId);
                  T controller = 
                      clazz.cast(
                          Component.getInstance(clazz, getScopeType(clazz), true));
                  //We have to do this to ensure Bijection interceptor fires
                  if (controller instanceof AbstractSeamComponent) {
                      ((AbstractSeamComponent)controller).gekkoInit();
                  }
                  switchConversation(currentConversation);
              }
              
              public void switchConversation(String conversationIdToGoTo) {
                  if (log.isTraceEnabled()) {
                      log.trace("Switching to conversation: " + conversationIdToGoTo);
                  }
          
                  Validate.isTrue(allConversations.contains(conversationIdToGoTo),
                      "NavigationManager does not know about the conversation " +
                      "with id:" + conversationIdToGoTo);
                  
                  if (!Manager.instance().switchConversation(conversationIdToGoTo)) {
                      throw new IllegalStateException("Conversation was not switched");
                  }
                  
                  if (!conversationStack.contains(conversationIdToGoTo)) {
                      conversationStack.push(conversationIdToGoTo);
                  }
              }
              
              public <T extends AbstractSeamComponent> T 
                  getController(Class<T> clazz)
              {
                  Validate.notNull(clazz, "param clazz cannot be null");
                  T controller = NavigationUtils.getSeamBean(clazz);
                  Validate.notNull(controller, "Controller of type: " + clazz.getSimpleName() + 
                      "could not be found in conversation: " + getCurrentConversationId());
                  return controller;
              }
                
          }
          



          See next post for more details and classes

          • 17. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
            kragoth

            Ok, so part 2.


            This class is probably the most used class in my Navigation framework.
            The setupNaviationAndForward method is used every time you want to navigation from one module to another. (Module being synonymous for conversation). I'll give examples in a sec.


            package gekko.web.navigation;
            
            import ...*
            
            public final class NavigationUtils {
                
                private NavigationUtils() {
                    //all static methods
                }
            
                /**
                 * There really should be no need to call this unless you are doing
                 * some very weird navigation. This is really designed for
                 * NavigationManager. Check out the AbstractNavigation classes
                 * and you should find a type of navigation to suite your needs
                 * without calling this method. Be warned that if you do use this method
                 * and are not familiar with how conversations and NavigationManager
                 * fit together this could cause you some unusual bugs.
                 * 
                 */
                public static <T> T getSeamBean(Class<T> clazz) {
                    T seamBean = clazz.cast(Component.getInstance(clazz,
                        ScopeType.CONVERSATION, false));
                    if (seamBean == null) {
                       seamBean = clazz.cast(Component.getInstance(clazz,
                            ScopeType.SESSION, false));
                       if(null == seamBean){
                           throw new IllegalStateException("Could not find Seam Bean of type: " + clazz.getSimpleName());
                       }
                    }
                    
                    return seamBean;
                }
                
                @SuppressWarnings("unchecked")
                public static <F extends AbstractSeamComponent> Class<F> 
                getForwardControllerType(AbstractSimpleForwardBackNavigation<F, ?> navigation)
                {
                    ParameterizedType genericSuperclass = 
                        (ParameterizedType)navigation.getClass().getGenericSuperclass();
                    return (Class<F>)genericSuperclass.getActualTypeArguments()[0];
                }
                
                @SuppressWarnings("unchecked")
                public static <BACK extends AbstractSeamComponent> Class<BACK> 
                getBackControllerType(AbstractSimpleForwardBackNavigation<?, BACK> navigation)
                {
                    ParameterizedType genericSuperclass = 
                        (ParameterizedType)navigation.getClass().getGenericSuperclass();
                    return (Class<BACK>)genericSuperclass.getActualTypeArguments()[1];
                }
                
                @SuppressWarnings("unchecked")
                public static <BACK extends AbstractSeamComponent> Class<BACK> 
                getBackControllerType(AbstractBackNavigation<BACK> navigation)
                {
                    ParameterizedType genericSuperclass = 
                        (ParameterizedType)navigation.getClass().getGenericSuperclass();
                    return (Class<BACK>)genericSuperclass.getActualTypeArguments()[0];
                }
                
                public static <F extends AbstractSeamComponent>
                void setupNavigationAndForward(
                    ForwardNavigation<F> forwardNav)
                {
                    Validate.notNull(forwardNav, "param forwardBacknav cannot be null");
                    Validate.isTrue(Conversation.instance().isLongRunning(), "doNavigation cannot be used outside a long running conversation.");
                    
                    final NavigationManager navMan = getSeamBean(NavigationManager.class);
            
                    Class<F> clazz =
                        forwardNav.getForwardControllerClass();
                    String forwardConversationId = forwardNav.getForwardConversationId();
                    
                    //Only create the new conversation if it doesn't already exist
                    if (forwardConversationId == null || 
                        !navMan.conversationExists(forwardConversationId))
                    {
                        forwardConversationId = navMan.createConversation();
                    }
                    
                    navMan.createController(forwardConversationId, clazz);
                    
                    navMan.switchConversation(forwardConversationId);
                    
                    F controller = navMan.getController(clazz);
             
                    forwardNav.setForwardConversationId(forwardConversationId);
                    forwardNav.navigateForward(controller);
                }
            
            }
            



            As you can see that utility references a Abstract class. Well, here it is.


            package gekko.web.navigation;
            
            import ...*
            
            public abstract class AbstractSimpleForwardBackNavigation
                <F extends AbstractSeamComponent, BACK extends AbstractSeamComponent>
                implements ForwardNavigation<F>, BackNavigation
            {
                private String forwardConversationId;
                private BACK backController;
                private final String backConversationId;
                protected final NavigationManager navMan;
                
                public AbstractSimpleForwardBackNavigation(
                    String forwardConversationId, 
                    String backConversationId,
                    NavigationManager navMan)
                {
                    Validate.notNull(navMan, "param navMan cannot be null");
                    Validate.notNull(backConversationId, "param backConversationId cannot be null");
                    
                    this.forwardConversationId = forwardConversationId;
                    this.backConversationId = backConversationId;
                    this.navMan = navMan;
                }
            
                @SuppressWarnings({"unchecked"})
                @Override
                public Class<F> getForwardControllerClass() {
                    ParameterizedType genericSuperclass =
                        (ParameterizedType)this.getClass().getGenericSuperclass();
                    return (Class<F>) genericSuperclass.getActualTypeArguments()[0];
                }
            
                public AbstractSimpleForwardBackNavigation(
                    String forwardConversationId, 
                    String backConversationId)
                {
                    this(forwardConversationId, backConversationId, NavigationUtils.getSeamBean(NavigationManager.class));
                }
            
            
                public AbstractSimpleForwardBackNavigation(String backConversationId) {
                    this(null, backConversationId);
                }
                
                @Override
                public String getForwardConversationId() {
                    return forwardConversationId;
                }
            
                @Override
                public void setForwardConversationId(String forwardConversationId) {
                    this.forwardConversationId = forwardConversationId;
                }
            
                public BACK getBackController() {
                    return backController;
                }
            
                public void setBackController(BACK backController) {
                    this.backController = backController;
                }
            
                public String getBackConversationId() {
                    return backConversationId;
                }
                
                @Override
                public abstract void navigateForward(F controller);
            
                @Override
                public void navigateBack() {
                    navMan.switchConversation(getBackConversationId());
                    BACK backCntrl =
                        navMan.getController(NavigationUtils.getBackControllerType(this));
                    back(backCntrl);
                }
            
                protected abstract void back(BACK backController);
            
                /**
                 * This should be called from within the {@link #navigateForward(gekko.web.module.AbstractSeamComponent)}
                 * method, it "undoes" what the navman did (pushed another conversation
                 * on the stack) so that you can sensibly handle arbitrary exceptions
                 * from the calling module that occur during navigation.
                 */
                protected void abortNavigation() {
                    navMan.switchConversation(this.getBackConversationId());
                    navMan.removeAllNestedConversations(this.getBackConversationId());
                }
            }
            



            Hopefully after you get familiar with the code you can see that it has been written to allow for wizard type navigation. (Not tested)



            So, I'll give an example of how this is used.
            This is an example of me navigating from my TenureSearch Module, to my TenureMaintenance Module.


                public void viewTenure(final UnauthTenure tenure) {
                    super.setRefreshResults(true);
                    
                    NavigationUtils.setupNavigationAndForward(
                        new AbstractSimpleForwardBackNavigation<TenureKeyController, TenureSearchController>(this.getConversationId()){
                            @Override
                            public void navigateForward(TenureKeyController controller) {
                                controller.gotoMaintainTenure(tenure, this);
                            }
                            
                            @Override
                            public void back(TenureSearchController backController) {
                                navMan.redirectToPage(
                                    getBackConversationId(),
                                    TENURE_SEARCH_VIEW_ID,
                                    null);
                            }
                        });
                }
            



            The navigation system is pretty complex to allow for all sorts of weird and wonderful types of navigation. I many different subclasses and implementations of Forward and Back navigation. I have a lookup style navigation that will pass a param back to the previous conversation/module as well... I'll give an example of that too.
            In the next reply :P

            • 18. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
              kragoth

              One thing I forgot to mention! All my Seam beans are subclasses of this AbstractClass.


              public class AbstractSeamComponent implements Serializable {
                  
                  private static final Logger log = 
                      LogManager.getLogger(AbstractSeamComponent.class);
                    
                  private String conversationId;
                  
                  /**
                   * This method is used by the Gekko Infrastructure to deal with
                   * Seam beans that are instantiated by the NavigationManager.
                   * This method exists to ensure that all the SEAM injection and
                   * outjection will occur straight after the creation of an
                   * AbstractSeamComponent.
                   * 
                   * Do not remove this method unless you have an alternative
                   * solution.
                   */
                  public void gekkoInit() {
                      if(log.isTraceEnabled()) {
                          log.trace("Gekko init method called.");
                      }
                  }
              
                  public String getConversationId() {
                      return conversationId;
                  }
              
                  public void setConversationId(String conversationId) {
                      this.conversationId = conversationId;
                  }
              
              }
              



              So that explains where the this.getConversationId() is coming from in the above example.


              Now the more interesting example:


                  public void selectControlLevelButtonClicked() {
                      NavigationUtils
                          .setupNavigationAndForward(
                              new AbstractSimpleLookup
                                  <FinancialGroupActionBean, 
                                  AccountMaintenanceController, 
                                  ControlLevel>
                              (this.getConversationId())
                          {
                              @Override
                              public void navigateForward(FinancialGroupActionBean controller)
                              {
                                  controller.lookupFinancialGroup(this);
                              }
              
                              @Override
                              public void selectEntity(
                                  AccountMaintenanceController backController,
                                  ControlLevel entity)
                              {
                                  backController.getCurrentAccount().setControlLevel(entity);
                                  FacesMessages.instance().clear();
                                  navMan.redirectToPage(getBackConversationId(),
                                      ACCOUNT_DETAILS_VIEW_ID,
                                      ACCOUNT_DETAILS_VIEW_BREADCRUMB_ID,
                                      backController.getAccountDisplayParam());
                              }
              
                              @Override
                              protected void back(AccountMaintenanceController backController)
                              {
                                  navMan.redirectToPage(getBackConversationId(),
                                      ACCOUNT_DETAILS_VIEW_ID,
                                      ACCOUNT_DETAILS_VIEW_BREADCRUMB_ID,
                                      backController.getAccountDisplayParam());
                              }
                          });
                  }
              



              So here we have 1 way of navigating forward but, 2 ways of navigating back. One where we just come straight back, and the other where we pass back a param. Here is the other side to this call so you can see what's happening.


                  public void lookupFinancialGroup(Lookup<ControlLevel> lookup) {
                      this.lookup = lookup;
                      init(); //Just load up some data
                      navMan.redirectToModule(
                          getConversationId(), 
                          FINANCIAL_GROUP_VIEW_ID, 
                          FINANCIAL_GROUP_VIEW_BREADCRUMB_ID,
                          null);
                  }
              



              So if they click the select button on this page then it will navigate back passing the entity they selected using this method.


               
                  public void selectFinancialGroupClick(ControlLevel fg){
                      lookup.selectEntity(fg);
                  }
              



              Otherwise if they just hit the Cancel button then just do this:


                  public void cancelButtonClicked(){
                      lookup.cancel();
                  }
              



              OK, so now the relevant Interfaces/superclasses to work all that out!


              package gekko.web.navigation;
              
              import ...*
              
              public interface Lookup<T extends PersistentEntity> extends Serializable {
                  void selectEntity(T selectedEntity);
                  void cancel();
              }
              



              package gekko.web.navigation;
              
              import ...*
              
              public abstract class AbstractSimpleLookup<F extends AbstractSeamComponent, BACK extends AbstractSeamComponent, S extends PersistentEntity>
                  extends AbstractSimpleForwardBackNavigation<F, BACK> implements Lookup<S>
              {
              
                  public AbstractSimpleLookup(String forwardConversationId, String backConversationId)
                  {
                      super(forwardConversationId, backConversationId);
                  }
                  
                  public AbstractSimpleLookup(String backConversationId)
                  {
                      super(backConversationId);
                  }
                  
                  @Override
                  public void selectEntity(S entity) {
                      navMan.switchConversation(getBackConversationId());
                      BACK backCntrl = navMan.getController(NavigationUtils.getBackControllerType(this));
                      selectEntity(backCntrl, entity);
                  }
                  
                  @Override
                  public void cancel() {
                      navigateBack();
                  }
                  
                  public abstract void selectEntity(BACK backController, S entity);
              
              }
              



              Well, I think that about covers the basics of what I'm doing. I would love to make a simplified version of this that everyone can use but I just don't have time at the moment.


              I hope though that someone gets some help out of it.
              Feel free to ask any questions or tell me if I need to post some more code that I might have forgotten.


              Cheers,
              Tim

              • 19. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)

                Tim - Thanks for posting your navigation code; this is going to help me out enormously.


                You have my thanks!

                • 20. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)

                  Great! I'll try to build an example project based on this ASAP... I wonder if it would be a good idea to create a seam-navigation-contrib project in GoogleCode to share our experiments with this... what do you think?

                  • 21. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
                    asookazian

                    I am hoping that XML will die one day (as does every major technology - or at least refactored extensively - do you think OOP will ever die? :).


                    XML is great for implementing orthogonal concerns like interceptors and Spring AOP without modifying every class/component that needs that service (whether it's logging, transactional support, security, etc.)


                    But the main problem I have with XML are threefold:


                    1) lack of type-safety (although modern IDEs help with XSD integration, etc.)
                    2) lack of log statements
                    3) inability to debug


                    But I don't have a big problem (yet) with pages.xml configs (other than the lack of debugging)...


                    • 22. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
                      kragoth

                      What about the fact that xml makes it so much harder to follow your application's logic.


                      When I'm trying to follow navigation logic in an app based on pages.xml I look at the navigate method and see that it returns an outcome BasketSavedSucess now I have to go find the pages.xml that has this outcome. Assuming you are writing an app as big as the one I am currently on you will have more then 1 pages.xml so I have to search up the heirachy and find where the navigation outcome BasketSavedSuccess is and then look at what logic it calls, and what view it navigates to. Then I have to go to the method that it calls to find out what is actually happening during the redirect.


                      Compare this to my navigation code.


                      Find the navigation method and then follow this method in the anonymous subclass


                      public void navigateForward(FinancialGroupActionBean controller)
                      {
                          controller.lookupFinancialGroup(this);
                      }
                      



                      So when I follow to the lookupFinancialGroup method I will find all the application logic and the view that is navigated to right there. This saves on development time and debugging time in a big way.

                      • 23. Re: Crazy Idea?: Programmatic Navigation (Static or Dynamic)
                        kragoth

                        Francisco Peredo wrote on Aug 28, 2009 16:04:


                        Great! I'll try to build an example project based on this ASAP... I wonder if it would be a good idea to create a seam-navigation-contrib project in GoogleCode to share our experiments with this... what do you think?


                        Hey Francisco,


                        I'm happy to go with GoogleCode (I think I can access it from work :P). I don't know how much time I can contribute at the moment though as we are approaching crunch time. But, please by all means let me know if/where you set something up so I can contribute as I get time. Feel free to ask me any questions about the code.


                        Cheers,
                        Tim

                        1 2 Previous Next