martes, 9 de septiembre de 2014

Multilenguaje en una WebCenter Framework Portal

Hace tiempo publiqué una solución de multidioma basada en un ADF Phase Listener.
Sin embargo, esta solución tuvo ciertos bugs y problemas que me hicieron retirarla del Blog.

Plntilla por defecto con cambio de Idioma
Enlace a la versión en inglés

No me he olvidado de ello y ahora presento una nueva solución y aquí la traigo :).

Esta solución resuelve los siguientes paradigmas del multidioma:
  • Traducción de literales del portal.
  • Traducción de los Navigation Resource del Navigation Model según la Locale actual.
Se basa en:
  • Una solución de multidioma basada en preferencia de usuario (Cookie).
  • Un ADF Phase Listener para la invalidación del Navigation Model.

  Filtro y Cookie de preferencia de usuario

Implementación de un Java Filter que sobreescribirá las Locales permitidas según la Locale actual. Se apoya en un RequestWrapper para la sobrescritura de estas Locales. Por defecto, el lenguage es inglés.

public final class LocaleFilter implements Filter {
    
    /**
     * Filter Configuration
     */
    private FilterConfig _filterConfig = null;

    /**
     * Initializing the filter
     * @param filterConfig
     * @throws ServletException
     */
    public void init(FilterConfig filterConfig) throws ServletException {
        _filterConfig = filterConfig;
    }

    /**
     * Destroy the filter
     */
    public void destroy() {
        _filterConfig = null;
    }

    /**
     * Check the Cookie and overwrite the default language given by the browser
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException,
                                                   ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        String preferredLocale = this.getPreferredLocaleFromCookies(req);
        LocaleRequestWrapper localeReqWrapp = new LocaleRequestWrapper(req,preferredLocale);
        chain.doFilter(localeReqWrapp, response);
    }

    /**
     * Get the preferredLocale from the cookies
     * @param req
     * @return String format locale from the configured cookie
     */
    private String getPreferredLocaleFromCookies(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        boolean found = false;
        int i = 0;
        Cookie cookie = null;
        String prefLang = null;
        if (cookies != null) {
            while (i < cookies.length && !found) {
                cookie = cookies[i];;
                if (cookie.getName().equalsIgnoreCase(CustomLocaleUtils.LANG_COOKIE)) {
                    prefLang = cookie.getValue();
                    found = true;
                }
                ++i;
            }
        }
        return prefLang;
    }
}


public final class LocaleRequestWrapper extends HttpServletRequestWrapper{
    
    /**
     * Locale to apply
     */
    private Locale locale = null;


    /**
     * Initializes de wrapped request setting the language to be used
     * @param req
     * @param lang
     */
    public LocaleRequestWrapper(HttpServletRequest req, String lang) {
        super(req);
        if (StringUtils.isNotEmpty(lang)) {
            // Preferred locale by the user (Cookie)
            locale = LocaleUtils.toLocale(lang);
        } else {
            // Default locale, english hardcoded
            locale = new Locale("en");
        }
    }

    /**
     * Setting the language to be shown instead of the browser Accept-Languages
     * @return Enumeration with just the desired locale
     */
    @Override
    public Enumeration getLocales() {
        Vector locales = new Vector();
        locales.add(locale);
        return locales.elements();
    }


Una clase auxiliar de ayuda para la escritura de Cookies.

public final class CustomLocaleUtils {
    
    /**
     * Name of the cookie storing the user preference
     */
    public static final String LANG_COOKIE = "merchan_lang";
    
    /**
     * Services class. Can not be instantiated
     */
    private CustomLocaleUtils() {
        super();
    }
    
    
    /**
     * Set a new preferred locale
     * @param lang
     */
    public static void setCookieLang(Locale lang) {
        HttpServletResponse response = (HttpServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse();
        // Get the context-root because is the same as the cookie-path in weblogic.xml
        ServletContext appContext = (ServletContext)ADFContext.getCurrent().getEnvironment().getContext();
        // Write the cookie in the cookie path /MyApp
        Cookie langCookie = new Cookie(LANG_COOKIE,lang.getLanguage());
        //langCookie.setPath(appContext.getContextPath());
        langCookie.setPath("/");
        response.addCookie(langCookie);
    }
}


Un manejador de la localización para mantener que Locale es la actual e interactuar con la interfaz para modificar la misma.

public final class LocaleHandler {
    /**
     * List of the supported Locales by the WebCenter Portal Application
     */
    private List<SelectItem> supportedLocales;

    /**
     * Holds the current locale
     */
    private Locale currentLocale;
    
    /**
     * Flag to indicate if the language was changed
     */
    private boolean changed;

    /**
     * Default constructor
     */
    public LocaleHandler() {
        super();
        // Initialize the supportedLocales list
        Iterator<Locale> iteratorSupportedLocales = FacesContext.getCurrentInstance().getApplication().getSupportedLocales();
        supportedLocales = new ArrayList();
        SelectItem itemLocale = null;
        while (iteratorSupportedLocales.hasNext()) {
            Locale locale = iteratorSupportedLocales.next();
            itemLocale = new SelectItem(locale, locale.getDisplayName(), locale.getDisplayName());
            supportedLocales.add(itemLocale);
        }
        currentLocale = ADFContext.getCurrent().getLocale();
        changed = false;
    }

    /**
     * Change the language from a given action of an JSF ActionListener
     * @param ae
     */
    public void changeLanguageEvent(ActionEvent ae) {
        CustomLocaleUtils.setCookieLang(currentLocale);
        NavigationModel navModel = SiteStructureContext.getInstance().getCurrentNavigationModel();
        FacesContext fctx = FacesContext.getCurrentInstance();
        ExternalContext ectx = fctx.getExternalContext();
        try {
            NavigationResource navResource = navModel.getCurrentSelection();
            String prettyUrl = ectx.getRequestContextPath() + navResource.getGoLinkPrettyUrl();
            changed = true;
            ectx.redirect(prettyUrl);
         } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Set changed
     * @param changed
     */
    public void setChanged(boolean changed) {
        this.changed = changed;
    }

    /**
     * Get changed flag
     * @return boolean
     */
    public boolean isChanged() {
        return changed;
    }

    /**
     * Get the supported locales by the WebCenter Application
     * @return List of locales
     */
    public List <SelectItem> getSupportedLocales() {
        return supportedLocales;
    }

    /**
     * Set the current locale in a variable to easier access
     * @param currentLocale
     */
    public void setCurrentLocale(Locale currentLocale) {
        this.currentLocale = currentLocale;
    }

    /**
     * Get the current locale
     * @return
     */
    public Locale getCurrentLocale() {
        return currentLocale;
    }
}


El siguiente fragmento de código hace uso de este Managed Bean para el cambio de idioma (Page Template).



   
      
        
      
      
      



Esto es suficiente para el multidioma?. No. Si un Navigation Model es configurado para que sus elementos de navegación usen un Resource Bundle para obtener los títulos de los nodos hace falta también invalidar la caché del Navigation Model para que estos títulos sean de nuevo obtenidos.

Invalidación de la caché del Navigation Model

Implementación de un ADF Phase Listener que invalida el Navigation Model en la fase PREPARE_MODEL en caso de que el idioma haya sido cambiado.

public class LocalePhaseListener implements PagePhaseListener {
    public LocalePhaseListener() {
        super();
    }

    public void afterPhase(PagePhaseEvent pagePhaseEvent) {
    }

    public void beforePhase(PagePhaseEvent pagePhaseEvent) {
        if (Lifecycle.PREPARE_MODEL_ID == pagePhaseEvent.getPhaseId()) {
            LocaleHandler localeHandler = (LocaleHandler)ADFContext.getCurrent().getSessionScope().get("localeHandler");
            if (localeHandler != null && localeHandler.isChanged()) {
                SiteStructureUtils.invalidateDefaultNavigationModelCache();
                localeHandler.setChanged(false);
            }
        }
    }
}

Ejemplo descargable a través de mi repositorio GitHub.

jueves, 4 de septiembre de 2014

Recursos / Assets de Framework Portal despues de un re-despliegue

Los recursos / assets de WebCenter Framework Portal son referenciados en el archivo generic-site-resources.xml que se encuentran en la ruta de MDS bajo el GUID de portal por defecto.

/oracle/webcenter/siteresources/scopedMD/s8bba98ff_4cbb_40b8_beee_296c916a23ed

Versión en inglés

La administración de WebCenter Framework Portal permite la edición y gestión de los recursos de portal que estan registrados en dicho archivo.

Recursos / Assets de Portal



En el caso de WebCenter Portal, hay múltiples de ellos, uno por cada GUID de Portal creado con Portal Builder (además de el por defecto).

Ejemplo de fragmento del archivo por defecto de generic-site-resources.xml


   
      
         
      
      
         
      
   
   
      
         
      
      
         
      
      
         
      
      
         
      
      
         
      
      
         
      
      
         
      
   
   
   
      
         
            
            
            
            
            
         
      
      
         
            
            
            
            
            
         
      
    ...
   
   
   



Este post se centra en un "error" muy común cuando se desea redesplegar una aplicación de WebCenter Framework Portal.

"He redesplegado una aplicación de WebCenter Framework Portal y se han perdido los recursos que cree en Runtime! Qué ha pasado?."

Cuando se empaqueta una aplicación de WebCenter Framework Portal se genera un archivo .EAR a través de los scripts ANT de OJDeploy de JDeveloper. Esto crea e introduce en el .EAR un archivo llamado AutoGeneratedMar.mar con los recursos / assets de Portal en tiempo de diseño, incluyendo el famoso generic-site-resources.xml local.

Si no se realiza ninguna modificación a este empaquetado entonces durante el re-despliegue de la aplicación de WebCenter Framework Portal se sobreescribiran todos los archivos que contenga el archivo AutoGeneratedMar.mar y entre ellos el generic-site-resources.xml.

Qué ha pasado con los assets creados en Runtime

Básicamente siguen existiendo, pero al no estar referenciados en el generic-site-resources.xml en la Administración de WebCenter Framework Portal no apareceran como recursos (aunque pueden seguir siendo configurados a través de su path de MDS en la pestaña de Configuration).

Soluciones 

Existen dos soluciones para re-desplegar:
  • Mantener la aplicación de WebCenter Framework Portal de JDeveloper actualizada importando los recursos de portal creados en Runtime a tiempo de Diseño. Con ello se mantendrá sincronizado el archivo generic-site-resources.xml
  • Añadir el archivo mds-transfer-config.xml a la carpeta META-INF dentro del archivo AutoGeneratedMar.mar para indicar qué archivos de MDS no deben ser sobreescritos.
Ejemplo del contenido del archivo mds-transfer-config.xml para prevenir cambios en generic-site-resources.xml


  
    
      
     
      
    
  


martes, 2 de septiembre de 2014

ADF11gR1: Task Flow ejecutados en Dialogo/Popup

Hay dos modos de ejecutar un Task Flow en popup.

Versión en inglés
  • Generando la Bounded Task Flow basada en páginas JSPX en lugar de fragmentos JSFF. Con ello se consigue que el Task Flow pueda ser llamado como diálogo en una actividad de otro Task Flow.

    Task Flow invocado como inline-popup

  • Usando la Bounded Task Flow como Region dentro de un af:popup
    
        
          
            
              
            
            
              
              
                
              
            
          
        
        
        
          
          
            
          
        
     
    

Ventajas e Inconvenientes de cada solución

Solución basada en TF-Call JSPX

Ventajas:
  • Se puede utilizar el framework de ADF para llamar en módo diálogo el Task Flow.
  • El Task Flow puede devolver valores a su vuelta usando el Return Values.
Inconvenientes:
  • Mayor dificultad para aplicar estílos específicos al Diálogo/Popup.
  • El Task Flow no puede ser embebido como Region.

Solución basada en Region embebida en un af:popup

Ventajas:
  • Se puede reutilizar como Region en otras Unbounded/Bounded Task Flow.
  • Facilidad para aplicar styleClass específicos para af:popup y af:dialog.
Inconvenientes:
  • No se puede devolver valores en el Return Values. Se debe realizar mediante Eventos Contextuales y un regionNavigationListener.
  • Para refrescar el Task Flow ejecutado dentro del popup. No basta con una política de refresco en Refresh y Refresh Condition. Hace falta controlar la activación del Task Flow cuando éste se abre y se cierra.

El ejemplo

El ejemplo requiere conocimientos básicos de ADF 11gR1 y contiene ambas soluciones. A través de las siguientes páginas de test se puede seguir el ejemplo:
  • testPopupInTFCall.jspx: Ejecuta los Task Flow que demuestra como un TF es invocado en modo diálogo y, a su vez, devuelve valores utilizando su Return Value. Puntos importantes del ejemplo:
    • El Task Flow es invocado como TF-Call en modo dialogo. Por ello el botón de navgación hacia el dialogo debe tener useWindow="true".
       
      


    • Al tener configurado Return Values, se mapean a una variable de Page Flow Scope para poder ser usada por el fragmento.

      Return Values definidas para el Task Flow invocado como diálogo

      Mapeo del valor devuelto por el Task Flow a una variable local del Task Flow
  • testPopupInRegion.jspx: En el se encuentra una Task Flow que es invocada en un af:popup. Configurada la política de activación para que el Task Flow se active y se cierre según . Además, incluye el uso de Eventos Contextuales para obtener los datos de respuesta. Puntos importantes de esta parte:

    • Politica de activación del Task Flow para que se desactive y active según es abierto / cerrado el popup.
      Política de activación del Task Flow
    • Uso de eventos contextuales (disparado programaticamente) en el botón que cierra el diálogo / popup para devolver los datos al Task Flow llamador.

      
        
        
          
          
            
              
            
          
        
        
          
            
          
        
        
          
            
              
                
                  
                
              
            
          
        
      
      

      /**
       * Closer Picker action
       * @param ae Action Component used
       */
      public void performClose(ActionEvent ae) {
       
       // Execute the Contextual Event programmatically before of unloading the popup
       BindingContainer bindingContainer = BindingContext.getCurrent().getCurrentBindingsEntry();
       JUEventBinding eventBinding = (JUEventBinding)bindingContainer.get("returnDialogEventBinding");  
       ActionListener actionListener = (ActionListener)eventBinding.getListener();  
       actionListener.processAction(ae);  
       
       // Check the Input Parameter if the TF has to be considered as executed in popup
       AdfFacesContext adfFacesContext = AdfFacesContext.getCurrentInstance();
       Map<String,Object> flowScope = adfFacesContext.getPageFlowScope();
       Boolean isPopup = (Boolean)flowScope.get("isPopup");
       if (isPopup != null && isPopup) {
        this.closePopup(ae);
       }
      }
      
      /**
       * Auxiliar method to close the popup
       * @param ae
       */
      private void closePopup(ActionEvent ae) {
       // Looking for the RichPopup parent of picker
       UIComponent component = ae.getComponent();
       while (!(component instanceof RichPopup))
        component = component.getParent();
       if (component != null)
        ((RichPopup)component).cancel();
      }
      

      public final class ContextualEventHandlerDC {
          
          /**
           * Default Constructor
           */
          public ContextualEventHandlerDC() {
              super();
          }
          
          /**
           * Get the list of names returned by the Contextual Event
           * @param names
           */
          public void handleReturnEvent(List<String> names) {
              for (String s: names) {
                  System.out.println(s);
              }
              AdfFacesContext.getCurrentInstance().getPageFlowScope().put("returnedNames", names);
          }
      }
      
Enlace al repositorio GitHub del Ejemplo

Ejemplo desarrollado en JDeveloper 11.1.1.7