Package org.olat.core.gui.components

Source Code of org.olat.core.gui.components.ValidatingVisitor

/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) 1999-2006 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/

package org.olat.core.gui.components;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.dispatcher.mapper.MapperRegistry;
import org.olat.core.gui.GUIInterna;
import org.olat.core.gui.GlobalSettings;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.Windows;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.control.ChiefController;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.DispatchResult;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.JSAndCSSAdder;
import org.olat.core.gui.control.JSAndCSSAdderImpl;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.history.HistoryEntry;
import org.olat.core.gui.control.info.WindowControlInfo;
import org.olat.core.gui.control.winmgr.Command;
import org.olat.core.gui.control.winmgr.CommandFactory;
import org.olat.core.gui.control.winmgr.MediaResourceMapper;
import org.olat.core.gui.control.winmgr.WindowBackOfficeImpl;
import org.olat.core.gui.exception.MsgFactory;
import org.olat.core.gui.media.AsyncMediaResponsible;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.media.RedirectMediaResource;
import org.olat.core.gui.media.ServletUtil;
import org.olat.core.gui.render.RenderResult;
import org.olat.core.gui.render.Renderer;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.render.URLBuilder;
import org.olat.core.gui.render.ValidationResult;
import org.olat.core.gui.render.intercept.InterceptHandler;
import org.olat.core.gui.render.intercept.InterceptHandlerInstance;
import org.olat.core.gui.themes.Theme;
import org.olat.core.gui.util.ReusableURLHelper;
import org.olat.core.helpers.Settings;
import org.olat.core.id.context.BusinessControl;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLATRuntimeException;
import org.olat.core.logging.Tracing;
import org.olat.core.util.component.ComponentTraverser;
import org.olat.core.util.component.ComponentVisitor;
import org.olat.testutils.codepoints.server.Codepoint;

/**
* Description: <br>
*
* @author Felix Jost
*/
public class Window extends Container {
  private static final int MAX_HISTORY_ENTRIES = 100;
 
  private static final String LOG_SEPARATOR = "^$^";
  /**
   * old time stamp call, but no asyncmediaresponsible
   * <code>OLDTIMESTAMPCALL</code>
   */
  public static final Event OLDTIMESTAMPCALL = new Event("ots");
  /**
   * while dispatching: component with id not found
   * <code>COMPONENTNOTFOUND</code>
   */
  public static final Event COMPONENTNOTFOUND = new Event("cnf");
  /**
   * fired when the dispatch cycle (dispatch to a component) is finished
   */
  public static final Event END_OF_DISPATCH_CYCLE = new Event("eodc");

  /**
   * fired before inline (text/html computed response) takes place
   */
  public static final Event BEFORE_INLINE_RENDERING = new Event("before_inline_rendering");

  /**
   * fired after the response has been rendered into a string (but not delivered to the client yet)
   */
  public static final Event AFTER_INLINE_RENDERING = new Event("after_inline_rendering");

   
  public static final Event AFTER_VALIDATING = new Event("before_validate");

  /**
   * fired just before the targetcomponent.dispatch takes places
   */
  public static final Event ABOUT_TO_DISPATCH = new Event("about_to_dispatch");
 
  private String uriPrefix;
  private Container contentPane;
  private String latestTimestamp;
  private AsyncMediaResponsible asyncMediaResponsible;
  private String instanceId;
  private int timestamp = 1; // used to find out when a user has called
  // back/forward/reload in the browser and to detect
  // asyncmedia resources
 
  private Theme guiTheme;
 
  private boolean validatingCausedRerendering = false;
 
  // for debugging and errortracing reasons
  private Component latestDispatchedComp;
  private String latestDispatchComponentInfo = null;
 
  // wbackoffice reference
  private WindowBackOfficeImpl wbackofficeImpl;
  // mutex for rendering
  private final Object render_mutex = new Object();
  // delegate for css and js includes
  private final JSAndCSSAdderImpl jsAndCssAdder;
 
  private List<HistoryEntry> history = new ArrayList<HistoryEntry>();
  private int historyPos = -1;
 

  private Map<String, Object> attributes = new HashMap<String, Object>();
 
  /**
   * @param name
   * @param chiefController
   */
  public Window(String name, WindowBackOfficeImpl wbackoffice) {
    super(name);
    this.wbackofficeImpl = wbackoffice;
    this.jsAndCssAdder = wbackoffice.createJSAndCSSAdder();
    // set default theme
    Theme myTheme = new Theme(Settings.getGuiThemeIdentifyer());
    setGuiTheme(myTheme);
  }
 
  public Component getJsCssRawHtmlHeader() {
    return jsAndCssAdder.getJsCssRawHtmlHeader();
  }

  /**
   *
   * @param guiThemeBaseUri the URI of the base folder of the current Gui theme, r.g.  'http://www.myserver.com/olat/raw/themes/default/'
   */
  public void setGuiTheme(Theme guiTheme) {
    this.guiTheme = guiTheme;
  }
 
  /**
   * @return the current GUI theme
   */
  public Theme getGuiTheme() {
    return this.guiTheme;
  }
 
  /**
   * @return Container
   */
  public Container getContentPane() {
    return contentPane;
  }

  /**
   * Sets the contentPane.
   *
   * @param contentPane The contentPane to set
   */
  public void setContentPane(Container contentPane) {
    this.contentPane = contentPane;
  }

  /**
   * @see org.olat.core.gui.components.Container#getComponent(java.lang.String)
   */
  public Component getComponent(@SuppressWarnings("unused") String name) {
    throw new AssertException("please use getContentPane()");
  }

  /**
   * @see org.olat.core.gui.components.Component#dispatchRequest(org.olat.core.gui.UserRequest)
   */
  protected void doDispatchRequest(UserRequest ureq) {
    dispatchRequest(ureq, false);
  }

  /**
   * @param ureq
   * @param renderOnly
   */
  public void dispatchRequest(UserRequest ureq, boolean renderOnly) {
    HttpServletRequest request = ureq.getHttpReq();
    HttpServletResponse response = ureq.getHttpResp();
    String timestampID = ureq.getTimestampID();
    String componentID = ureq.getComponentID();
    //int browserT = timestampID == null? 0 : Integer.parseInt(timestampID);

    // case windowId timestamp componentId
    // --------------------------------------------
    //
    // 1 null     null null -> if (!renderOnly)-> error else doRenderOnly<br>
    // 2 invalid n/a n/a -> not handled here, but in Windows
    // 3 valid valid valid -> dispatch and further handling (check for new
    // window and res. media resource
    //  4 valid valid invalid -> no dispatch (silently) -> rerender (no res.
    // media res and no new window check needed)
    //   5 valid invalid n/a -> asyncRes == null? "doNotUseReload" :
    //     getAsyncRes==null? renderInline : serve resource
    //  6 valid null n/a -> no timestamp -> just rerender inline

    // defs:
    //  rerender: component validation and inline rendering

    // case 1:
    //     simply rerender, no dispatching
    // case 3:
    //    dispatch, check for new Window
    // case 5:
    //    check newWindow, serve resource/renderInline

    // order:
    // 1. check for timestamp: valid: case 3,4 ; invalid -> case 5, or -1 ->
    // indicator of just rendering and no revalidating needed
    // 2. dispatch to component, unless flag renderOnly in method sig., or
    // inlineRerender set (timestamps indicates)

    boolean inline = false;
    boolean validate = false;
    boolean checkNewWindow = false;
    boolean dispatch = false;
   
    // increase the timestamp, but not if we are in loadperformancemode: then all url's have
    // to work independant of previous ones -> when no increase: timestamp is always the same here
    boolean incTimestamp = !GUIInterna.isLoadPerformanceMode();
   
    MediaResource mr = null;
    final boolean isDebugLog = Tracing.isDebugEnabled(Window.class);
    StringBuilder debugMsg = null;
    long debug_start = 0;
    if (isDebugLog) {
      debug_start = System.currentTimeMillis();
      debugMsg = new StringBuilder("::winst:");
    }
    synchronized (this) { //o_clusterOK by:fj
      // sync dispatching per window to avoid rendering problems
      // when user repeateadly presses reload, and also to distribute bandwidth more
      // evenly.
      // postcondition: each controller's events are called by one gui-thread at a time only.
     
      GlobalSettings gsettings = wbackofficeImpl.getGlobalSettings();
      boolean bgEnab = gsettings.getAjaxFlags().isIframePostEnabled();
      //System.out.println("in window:");
      // -------------------------
      // ----- ajax mode ---------
      // -------------------------
      if (bgEnab && (ureq.getMode() & 1) == 1) {
        // first check on "ajax-command-was-not-in-hidden-iframe hint" -> if so, rerender the current window
        if (ureq.getParameter("o_win_jsontop") != null) {       
          renderOnly = true;
        } else {
          try {
           
            // if target in background (m = mode , 0.bit set)
            // 1.) do dispatch to component if component timestamp ok
           
            //REVIEW:PB: this will be the code allowing back forward navigation
            //--> boolean inlineAfterBackForward = false;
            // FIXME:fj:b avoid double traversal to find component again below         
            String s_compID = ureq.getComponentID();
            if (s_compID == null) throw new AssertException("no component id found in req:" + ureq.toString());
            // throws NumberFormatException if not a number
            long compID = Long.parseLong(s_compID);
            List foundPath = new ArrayList(10);
            Component target = ComponentHelper.findDescendantOrSelfByID(getContentPane(), compID, foundPath);
            boolean validForDispatching;
            if (target != null) { // the target was found
              int tst = target.getTimestamp();
              String cTimest = Integer.toString(tst, 10);
              String urlCTimest = ureq.getComponentTimestamp();
              validForDispatching = cTimest.equals(urlCTimest);
              if (!validForDispatching && Tracing.isDebugEnabled(this.getClass()) ) {
                Tracing.logDebug("Invalid timestamp: ureq.compid:"+ureq.getComponentID()+" ureq.win-ts:"+ureq.getTimestampID()+" ureq.comp-ts:"+ureq.getComponentTimestamp() + " target.timestamp:" + cTimest + " target=" + target, this.getClass());
              }
            } else {
              // the component was not found in the rendertree anymore.
              // this can happen e.g. on quick double-clicks, so that the dom-replacement-command never reaches the client.
              if (Tracing.isDebugEnabled(this.getClass())) Tracing.logDebug("no ajax dispatch: component not found (target=null)",this.getClass());
              validForDispatching = false;
            }
           
            /*
             * REVIEW:PB: this will be the code allowing back forward navigation
             * so far it is disabled
             * window fires OLDTIMESTAMPCALL event
             * --->
            if (!validForDispatching) {
              // user clicked back or forward after an ajax request which causes the history of the hidden iframe to go back one and post
              // an old request which has an old timestamp.
              int diff = findInHistory(ureq);
              // if not found in history: probably the case that a link got opened in a new window by the user by using the right-mouse-button.
              // in this case, we later send a full page reload
              //System.out.println("ajax back: diff "+diff);
              if (diff != 0) {
                wbackofficeImpl.browserBackOrForward(ureq, diff);
                inline = true;
                inlineAfterBackForward = true;
              }
            }
            <-------- */               
           
            // 2.) collect dirty components (top-down, return from sub-path when first dirty node met)
            // 3.) return to sender...
            boolean didDispatch = false;
            if (validForDispatching) {
              didDispatch = doDispatchToComponent(ureq, null)// FIXME:fj:c enable time stats for ajax-mode
           
             
            MediaResource mmr = null;
            //REVIEW:PB: this will be the code allowing back forward navigation
            //-----> if (didDispatch || inlineAfterBackForward) {
            if (didDispatch || !validForDispatching) {
              if (validForDispatching) {
                Window ww = ureq.getDispatchResult().getResultingWindow();
                if (ww != null) {
                  // a link which causes a new window to be openend should always
                  // a) have the normal mode set (not the ajax mode)
                  // b) have the target="_blank" attribute
                  // reason: in non-ajax-mode, a link has to know beforehand whether it opens in a new window or not.
                  // FIXME:fj:c think about bodyOnLoad -> win.open(new window url)
                  throw new AssertException("a link in ajax mode should never result in a new window");
                }
                mmr = ureq.getDispatchResult().getResultingMediaResource();
                if (mmr == null) {
                  inline = true;
                } else {
                  inline = false;
                }
              } //else {
                // not valid: fire oldtimestamp event and later rerender
                // fire OLDTIMESTAMPCALL after validating the components
                // because in the validating phase the voting for the
                // GUIMessage place is done.
                //}
             
             
              //REVIEW:PB: this will be the code allowing back forward navigation
              //-----> if (inline) {
              if (inline || !validForDispatching) {
                Container top = getContentPane();
                // always validate here, since we are never in the case of just rerendering (we are in the bg iframe)
                ValidatingVisitor vv = new ValidatingVisitor(gsettings, jsAndCssAdder);
                ComponentTraverser ct = new ComponentTraverser(vv, top, false);
                ct.visitAll(ureq);
                wbackofficeImpl.fireCycleEvent(Window.AFTER_VALIDATING);
                //fire possible OLDTIMESTAMPCALL after validation
                //validation does a voting about where the GUIMsg is rendered
                if(!validForDispatching){
                  // not valid: fire oldtimestamp event and later rerender
                  fireEvent(ureq, OLDTIMESTAMPCALL);
                }
                ValidationResult vr = vv.getValidationResult();
   
                boolean newJsCssAdded= vr.getJsAndCSSAdder().finishAndCheckChange();
                String newModUri = vr.getNewModuleURI();
                // !validForDispatching ||
                if (newJsCssAdded || newModUri != null) {
                  // send 302 redirect so the ajax-iframe's parent window gets reloaded to either include new js/css or to prepare the address bar
                  // url for asynchronous requests when delivering inline-contentpackaging.
                  // set window id to cur id, timestamp to current timestamp,
                  // component id to -1 -> indicates rerender
                  String uri = buildURIForRedirect(newModUri); // newModUri == null in case "just" new css or js libs have been added
                  // set this only for the first request (the .html request), but clear it afterwards for asyncmedia
                  validatingCausedRerendering = true;
                  Command rmrcom = CommandFactory.createParentRedirectTo(uri);
                  wbackofficeImpl.sendCommandTo(rmrcom);
                  //OLAT-4563: so the timestamp is not incremented, we do only a redirect
                  setDirty(false);
                } else {
                  // inline rendering by selectively replacing the dirty components in the dom tree of the browser
                  wbackofficeImpl.fireCycleEvent(Window.BEFORE_INLINE_RENDERING);
         
                  // Start by preparing the client: must be called prior to the
                  // other commands to not overwrite the form o2c dirty flag
                  // wich might be set by later commands
                  if (!this.isDirty()) {
                    wbackofficeImpl.sendCommandTo(CommandFactory.createPrepareClientCommand(wbackofficeImpl.getBusinessControlPath()));
                  }
                 
                  // Add the js and css files and related pre init commands
                  Command jscsscom = jsAndCssAdder.extractJSCSSCommand();
                  wbackofficeImpl.sendCommandTo(jscsscom);
                 
                  // Add the DOM replacement commands. Must be called after the
                  // js and css commands. Inline JS scripts might have
                  // dependencies to previously loaded js libs
                  if (this.isDirty()) {
                    // special case: when the window itself is dirty we require
                    // a full page refresh in any case
                    String reRenderUri = buildURIFor(this, timestampID, null);
                    Command rmrcom = CommandFactory.createParentRedirectTo(reRenderUri);
                    wbackofficeImpl.sendCommandTo(rmrcom);
                    this.setDirty(false);
                  } else {
                    // check for dirty child components in the component tree
                    Command co = handleDirties();
                   
                    //DUMP FOR EACH CLICK THE CURRENT JumpInPath -> for later usage and debugging.
                    //System.err.println("V^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^V");
                    WindowControl current = (WindowControl)wbackofficeImpl.getWindow().getAttribute("BUSPATH");
                    //System.err.println(current != null ? JumpInManager.getRestJumpInUri(current.getBusinessControl()) : "NONE");
                    //System.err.println("T^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^T");
                    wbackofficeImpl.fireCycleEvent(AFTER_INLINE_RENDERING);
                    if (co != null) { // see method handleDirties for the rare case of co == null even if there are dirty components;
                      wbackofficeImpl.sendCommandTo(co);
                    }
                  }
                }
              } else { // not inline
                if(!validForDispatching){
                  // not valid: fire oldtimestamp event
                  fireEvent(ureq, OLDTIMESTAMPCALL);
                }
                // not inline, new mediaresource
                // send it to the parent window (e.g. an excel download, but could also be a 302 redirect)
                // if the browser has e.g. pdf configured to be displayed inline, we want it to fill the whole area (self window), not the hidden iframe.
                // the same for 302.
                // -> send a command which offers a new location for the main window.
                // create a mapper which maps this mediaresource, and serves it once only
                MediaResourceMapper extMRM = new MediaResourceMapper();
                extMRM.setMediaResource(mmr);
                //FIXME:fj:b deregister old mapper, or reuse current one
                String res = MapperRegistry.getInstanceFor(ureq.getUserSession()).register(extMRM)+"/";
                // e.g. res = /olat/m/10001/
                Command rmrcom = CommandFactory.createParentRedirectForExternalResource(res);
                wbackofficeImpl.sendCommandTo(rmrcom);
              }
            } else { // not dispatched
              Tracing.logDebug("Found a valid timestamp but could not dispatch to component: ureq.compid:"+ureq.getComponentID()+" ureq.win-ts:"+ureq.getTimestampID()+" ureq.comp-ts:"+ureq.getComponentTimestamp() + " target.timestamp:" + target.getTimestamp() + " target=" + target, this.getClass());
              String reRenderUri = buildURIFor(this, timestampID, null);
              Command rmrcom = CommandFactory.createParentRedirectTo(reRenderUri);
              wbackofficeImpl.sendCommandTo(rmrcom);
            }
            MediaResource jsonmr = wbackofficeImpl.extractCommands(true);
            ServletUtil.serveResource(request, response, jsonmr);
          } catch (Throwable th) {
            // in any case, try to inform the user appropriately.
            // a) error while dispatching (e.g. db problem, npe, ...)
            // b) for inline: error while validating or json-rendering dirty components.
           
            // since an error has occured for a request which is targeted in the background iframe, we need to redirect to the error window.
            // create the error window
            try {
              Tracing.logDebug("Error in Window, rollback", getClass());
              DBFactory.getInstance().rollback();
           
              ChiefController msgcc = MsgFactory.createMessageChiefController(ureq, th);
              Window errWindow = msgcc.getWindow();
              // register window
              Windows.getWindows(ureq).registerWindow(errWindow);
              // redirect to the error window
              String newWinUri = buildRenderOnlyURIFor(errWindow);
              Command rmrcom = CommandFactory.createParentRedirectTo(newWinUri);
              wbackofficeImpl.sendCommandTo(rmrcom);
              MediaResource jsonmr = wbackofficeImpl.extractCommands(true);
              ServletUtil.serveResource(request, response, jsonmr);
            } catch (Throwable anotherTh) {
              Tracing.logError("Exception while handling exception!!!!", anotherTh, this.getClass());
            }
          }
          return;
        }
      }
     
      // -------------------------
      // ----- standard mode -----
      // -------------------------
      if (renderOnly || timestampID == null) {
        inline = true;
        validate = true;
      } else if (validatingCausedRerendering && timestampID.equals("-1")) {
        // the first request after the 302 redirect cause by a component validation
        // -> just rerender, but clear the flag for further async media requests
        validatingCausedRerendering = false;
        inline = true;
        validate = false; // no need to revalidate right now
        checkNewWindow = false;
        dispatch = false;
      else {
        // [POST: !renderOnly && timestampID != null]
        // if we had a inline rendering at least once (latestTimestamp is
        // set), then check for an old timestamp
        //System.out.println("dispatch normal: compid:"+ureq.getComponentID()+" win-ts:"+ureq.getTimestampID()+" comp-ts:"+ureq.getComponentTimestamp());
        if (latestTimestamp != null && !timestampID.equals(latestTimestamp)) {
          // this is not a link from the latest rendering, but from a previous
          // one, since it has a wrong timestamp parameter -> check for
          // asynchronous media
          if (asyncMediaResponsible == null) { // no async resp.
            // assume it to be a link from an old window (using browser back or
            // "open in new
            // window/tab" in the browser).
            if ((componentID != null && componentID.equals("-1")) || (ureq.getParameter("o_winrndo") != null)) {
              // just rerender
            else {
              // not a valid timestamp -> most likely a browser back or forward event (or a copy/paste of a url) ->

              // fire event to listening chiefcontroller
              fireEvent(ureq, OLDTIMESTAMPCALL);
              /*
               *
               * REVIEW:PB: this will be the code allowing back forward navigation
               * ---->
              // look at the timestamps more thoroughly.
              if (!timestampID.equals("-1")) {
                int diff = findInHistory(ureq);
                // diff == 0 -> reload (->ignore, so it will cause a simple rerendering)
                // diff < 0  -> browser-back
                // diff > 0  -> browser-forward
                //System.out.println("!!!!(normal) back: diff "+diff);
                wbackofficeImpl.browserBackOrForward(ureq, diff);
              } // else a 302 redirect of the main window -> simply rerender
              if (ureq.getComponentID() != null) {
                //System.out.println("normal: compid:"+ureq.getComponentID()+" win-ts:"+ureq.getTimestampID()+" comp-ts:"+ureq.getComponentTimestamp());
              } else {
                //System.out.println("special url - no component part (e.g. 302 redirect because of new req. js / css) compid:"+ureq.getComponentID()+" win-ts:"+ureq.getTimestampID()+" comp-ts:"+ureq.getComponentTimestamp());
              }
              validate = true;
              <------------- */
            }
            // just rerender current window
            inline = true;
            // do not increment timestamp so that e.g. url in a iframe remain valid
            incTimestamp = false;
          } else {
            // some component will take care of it for the moment, so be it
            mr = asyncMediaResponsible.getAsyncMediaResource(ureq);
            if (mr == null) { // indicates inline rendering
              inline = true;
              checkNewWindow = true; // an inline rendered async link should be
              // able to produce a new window
              validate = true;
            } else { // serve the resource.
              // all flags remain at their default value
            }
          }
        } else {
          // latestTimestamp == null || timestampID.equals(latestTimestamp)
         
          dispatch = true;
          checkNewWindow = true;
          validate = true;       
        }
      }
      // end of simple flagging.
      long dstart = 0;
      if (isDebugLog) {
        dstart = System.currentTimeMillis();
        long syncIntroDiff = dstart - debug_start;
        debugMsg.append("sync_bdisp:").append(syncIntroDiff).append(LOG_SEPARATOR);
      }
     
      if (dispatch) {
        boolean didDispatch = doDispatchToComponent(ureq, debugMsg);
        if (isDebugLog) {
          long dstop = System.currentTimeMillis();
          long diff = dstop - dstart;
          debugMsg.append("disp_comp:").append(diff).append(LOG_SEPARATOR);
          //Tracing.logDebug("componentdispatchtime: " + (dstop - dstart), Window.class);
        }
        if (didDispatch) { // the component with the given id was found
          mr = ureq.getDispatchResult().getResultingMediaResource();
          if (mr == null) {
            inline = true;
          } else {
            inline = false;
          }
        } else {
          // component with id was not found -> probably asynchronous thread changed flow ->
          // just rerender
          inline = true;
          dispatch = false;
          checkNewWindow = false;
          validate = true;
        }

      }

      if (checkNewWindow) {
        Window resWindow = ureq.getDispatchResult().getResultingWindow();
        if (resWindow != null) {
          // register it first, if not done before
          Windows ws = Windows.getWindows(ureq);
          if (!ws.isRegistered(resWindow)) {
            resWindow.setUriPrefix(uriPrefix);
            ws.registerWindow(resWindow);
          }
          // render initial state of new window by redirecting (302) to the new
          // window id. needed for asyncronous data like images loaded
         
          // todo maybe better delegate window registry to the windowbackoffice?
          URLBuilder ubu = new URLBuilder(uriPrefix, resWindow.getInstanceId(), String.valueOf(resWindow.timestamp), resWindow.wbackofficeImpl);
          StringOutput sout = new StringOutput(30);
          ubu.buildURI(sout, null, null);
          mr = new RedirectMediaResource(sout.toString());
          ServletUtil.serveResource(request, response, mr);
          if (isDebugLog) {
            long diff = System.currentTimeMillis() - debug_start;
            debugMsg.append("rdirnw:").append(diff).append(LOG_SEPARATOR);
            Tracing.logDebug(debugMsg.toString(), Window.class);
          }
          return;
        }
      }

      if (inline) {
          // do inline rendering.
         
          Container top = getContentPane();
          // validate prior to rendering, but only if the timestamp was not null
          // /
          // the component just got dispatched
          if (validate) { // do not validate if a previous validate lead to a
            // redirect; validating makes no sense here
            //long t1 = System.currentTimeMillis();
            ValidatingVisitor vv = new ValidatingVisitor(gsettings, jsAndCssAdder);
            ComponentTraverser ct = new ComponentTraverser(vv, top, false);
            ct.visitAll(ureq);
            wbackofficeImpl.fireCycleEvent(Window.AFTER_VALIDATING);
            ValidationResult vr = vv.getValidationResult();
            String newModUri = vr.getNewModuleURI();

            vr.getJsAndCSSAdder().finishAndCheckChange(); // ignore the return value since we are just about rendering anyway
         
            if (newModUri != null) {
              // send 302 redirect without dispatching, but just rerender
              // inline.
              // set window id to cur id, timestamp to current timestamp,
              // component id to -1 -> indicates rerender
              String uri = buildURIForRedirect(newModUri);
              MediaResource mrr = new RedirectMediaResource(uri);
              // set this only for the first request (the .html request), but clear it afterwards for asyncmedia
              validatingCausedRerendering = true;
              ServletUtil.serveResource(request, response, mrr);
              if (isDebugLog) {
                long diff = System.currentTimeMillis() - debug_start;
                debugMsg.append("rdirva:").append(diff).append(LOG_SEPARATOR);
                Tracing.logDebug(debugMsg.toString(), Window.class);
              }
              return;
            }
          }

         
          wbackofficeImpl.fireCycleEvent(BEFORE_INLINE_RENDERING);
          String result;
          synchronized(render_mutex) { //o_clusterOK by:fj
            // render now
            if (incTimestamp) timestamp++;
            String newTimestamp = String.valueOf(timestamp);
            // add the businesscontrol path for bookmarking:
            // each url has a part in it (the so called business path), which, in case of an invalid url or invalidated
            // session, can be used as a bookmark. that is, urls from our framework are bookmarkable, but require some little
            // coding effort: setting an appropriate business path and launching for each controller.
            // note: the businesspath may also be used as a easy (but of course not perfect) back-button-solution:
            // if the timestamp of a request is outdated, simply jump to its bookmarked business control path.
            URLBuilder ubu = new URLBuilder(uriPrefix, getInstanceId(), newTimestamp, wbackofficeImpl);
            RenderResult renderResult = new RenderResult();
           
            // if we have an around-component-interception
            // set the handler for this render cycle
            InterceptHandler interceptHandler = wbackofficeImpl.getInterceptHandler();
            if (interceptHandler != null) {
              InterceptHandlerInstance dhri = interceptHandler.createInterceptHandlerInstance();
              renderResult.setInterceptHandlerRenderInstance(dhri);
            }
           
            Renderer fr = Renderer.getInstance(top, top.getTranslator(), ubu, renderResult, gsettings);
            long rstart = 0;
            if (isDebugLog) {
              rstart = System.currentTimeMillis();
            }
            result = fr.render(top).toString();
            if (isDebugLog) {
              long rstop = System.currentTimeMillis();
              long diff = rstop - rstart;
              debugMsg.append("render:").append(diff).append(LOG_SEPARATOR);
            }
            if (renderResult.getRenderException() != null) throw new OLATRuntimeException(Window.class, renderResult.getLogMsg(),
                renderResult.getRenderException());
   
            // after rendering we know if some component awaits further async
            // calls
            // like images, so get a handler
            AsyncMediaResponsible amr = renderResult.getAsyncMediaResponsible();
            setAsyncMediaResponsible(amr); // if amr == null -> we are not
            // excepting
            // any async calls in the near future...
            latestTimestamp = newTimestamp;
          }
          if (isDebugLog) {
            long diff = System.currentTimeMillis() - debug_start;
            debugMsg.append("inl_comp:").append(diff).append(LOG_SEPARATOR);
          }
         
          //DUMP FOR EACH CLICK THE CURRENT JumpInPath -> for later usage and debugging.
          //System.err.println("VV^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^VV");
          WindowControl current = (WindowControl)wbackofficeImpl.getWindow().getAttribute("BUSPATH");
          //System.err.println(current != null ? JumpInManager.getRestJumpInUri(current.getBusinessControl()) : "NONE");
          //System.err.println("TT^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^TT");
          wbackofficeImpl.fireCycleEvent(AFTER_INLINE_RENDERING);
          ServletUtil.serveStringResource(request, response, result);
          if (isDebugLog) {
            long diff = System.currentTimeMillis() - debug_start;
            debugMsg.append("inl_serve:").append(diff).append(LOG_SEPARATOR);
          }
      }
      //else serve mediaresource, but postpone serving to when lock has been released,
      // otherwise e.g. a large download blocks the window, so that the user cannot click until the download is finished
    } // end of synchronized(this)
       
    if (!inline) {
      // it can be an async media resource, or a resulting mediaresource (image, an excel download, a 302 redirect, and so on.)
      if (isDebugLog) {
        long diff = System.currentTimeMillis() - debug_start;
        debugMsg.append("mr_comp:").append(diff).append(LOG_SEPARATOR);
      }
      ServletUtil.serveResource(request, response, mr);
      if (isDebugLog) {
        long diff = System.currentTimeMillis() - debug_start;
        debugMsg.append("mr_serve:").append(diff).append(LOG_SEPARATOR);
      }
    }
   
    if (isDebugLog) {
      // log the collected data now
      Tracing.logDebug(debugMsg.toString(), Window.class);
    }
  }

  /**
   * @param componentID
   * @param timestampID
   * @param componentTimestamp
   * @return -1 for back, 1 for forward, 0 for nothing/reload
   */
  private int findInHistory(UserRequest ureq) {
    for (int i = historyPos-1; i>=0; i--) {
      HistoryEntry he = history.get(i);
      if (he.isSame(ureq)) {
        // found "left" to the current position = left in time = back
        historyPos = i; // make ready for future comparisons
        return -1;
      }
    }
    // not found -> check forward
    int size = history.size();
    for (int i = historyPos+1; i<size; i++) {
      HistoryEntry he = history.get(i);
      if (he.isSame(ureq)) {
        // found "right" to the current position = right in time = forward
        historyPos = i; // make ready for future comparisons
        return 1;
      }
    }
    // not found, either reload, fake, error, or current entry -> return reload
    return 0;
  }
 
  /**
   * @param entry
   */
  private void addToHistory(HistoryEntry entry) {
    // clear old forward history
    int size = history.size();
    if (historyPos < size-1) { // we are not rightmost,
      // that is we have traversed back in history
      // -> clear forward history
      history.subList(historyPos+1, size).clear();
    }
    // now add the new entry
    history.add(entry);
    historyPos++;
   
    // restrict to 100 entries per user should be more than enough
    int nsize = history.size();
    if (nsize > MAX_HISTORY_ENTRIES) {
      // clear leftmost entry and adjust history position
      history.remove(0);
      historyPos--;
    }
   
  }

  /**
   * Set a window-scope variable
   *
   * @param key
   *            the identifier, must not be NULL
   * @param value
   *            the value, must not be NULL. Use removeAttribute() to remove a
   *            key
   */
  public void setAttribute(String key, Object value) {
    attributes.put(key, value);
  }

  /**
   * Get a window-scope variable
   *
   * @param key
   *            the identifier, must not be NULL
   * @return The object or NULL if no object exists for this key
   */
  public Object getAttribute(String key) {
    return attributes.get(key);
  }

  /**
   * Remove a windo-scope variable
   *
   * @param key
   *            the identifier, must not be NULL
   * @param the
   *            previously attribute that was set for this key or NULL when
   *            the key was not set at all
   */
  public Object removeAttribute(String key) {
    return attributes.remove(key);
  }
 

  /**
   * to be called by Window.java or the AjaxController only!
   * this method is synchronized on the Window instance
   *
   * @return a updateUI-Command or null if there are no dirty components (normally not the case for sync (user-click) request, but often the case
   * for pull request, since nothing has changed yet on the screen.
   */
  public Command handleDirties() {
    // need to sync to window, since the dispatching must be finished so that the render tree is stable before we collect the dirties.
    // more accurately, the synchronized is needed when other classes than window call this method.
    synchronized(this) {
      Command com = null;
      boolean isDebugLog = Tracing.isDebugEnabled(Window.class);
      StringBuilder debugMsg = null;
     
      final List<Component> dirties = new ArrayList<Component>();
      ComponentVisitor dirtyV = new ComponentVisitor() {
        public boolean visit(Component comp, UserRequest ureq) {
          boolean visitChildren = false;
          if (!comp.isVisible()) {
            // a component just made -visible- still needs to be collected (detected by checking dirty flag)
            if (comp.isDirty()) {
              dirties.add(comp);
              comp.setDirty(false)// clear manually here since this component will not be rendered
            }
          } else if (comp.isDirty()) {
            dirties.add(comp);
          } else {
            // visible and not dirty -> visit children
            visitChildren = true;
          }       
          return visitChildren;
        }};
      ComponentTraverser ct = new ComponentTraverser(dirtyV, getContentPane(), false);
      ct.visitAll(null);
     
      int dCnt = dirties.size();
      if (dCnt > 0) { // collect the redraw dirties command
        try {     
          JSONObject root = new JSONObject();
          root.put("cc", dirties.size());
          root.put("wts", timestamp);
          JSONArray ja = new JSONArray();
          root.put("cps", ja);
         
          GlobalSettings gsettings = wbackofficeImpl.getGlobalSettings();
         
          synchronized(render_mutex) { //o_clusterOK by:fj
            // we let all dirty components render themselves.
            // not offered (since not usability-useful) is the include of new js-libraries and css-libraries here, since this may invoke a screen reload
            // which disturbes the user and lets him/her loose the focus and the cursor.
            AsyncMediaResponsible amr = null;
 
            long rstart = 0;
            if (isDebugLog) {
              rstart = System.currentTimeMillis();
              debugMsg = new StringBuilder("update:").append(String.valueOf(dCnt)).append(";");
            }
           
            for (int i = 0; i < dCnt; i++) {
              Component toRender = dirties.get(i);
              boolean wasDomR = toRender.isDomReplaceable();
              if (!wasDomR) {
                throw new AssertException("cannot replace as dom fragment:"+toRender.getComponentName()+" ("+toRender.getClass().getName()+"),"+toRender.getExtendedDebugInfo());
              }
             
              Panel wrapper = new Panel("renderpanel");
              wrapper.setDomReplaceable(false); // to omit <div> around the render helper panel
              RenderResult renderResult = null;
              StringOutput jsol = new StringOutput();
              StringOutput hdr = new StringOutput();
              String result = null;
              try {
                toRender.setDomReplaceable(false);
                wrapper.setContent(toRender);
                String newTimestamp = String.valueOf(timestamp);
                URLBuilder ubu = new URLBuilder(uriPrefix,getInstanceId(), newTimestamp,wbackofficeImpl);

                renderResult = new RenderResult();

                // if we have an around-component-interception
                // set the handler for this render cycle
                InterceptHandler interceptHandler = wbackofficeImpl.getInterceptHandler();
                if (interceptHandler != null) {
                  InterceptHandlerInstance dhri = interceptHandler.createInterceptHandlerInstance();
                  renderResult.setInterceptHandlerRenderInstance(dhri);
                }

                Renderer fr = Renderer.getInstance(wrapper,null, ubu, renderResult, gsettings);

                jsol = new StringOutput();
                fr.renderBodyOnLoadJSFunctionCall(jsol,toRender);

                hdr = new StringOutput();
                fr.renderHeaderIncludes(hdr, toRender);

                long pstart = 0;
                if (isDebugLog) {
                  pstart = System.currentTimeMillis();
                }
                result = fr.render(toRender).toString();
                if (isDebugLog) {
                  long pstop = System.currentTimeMillis();
                  debugMsg.append(toRender.getComponentName()).append(":").append((pstop - pstart));
                  if (i < dCnt - 1)
                    debugMsg.append(",");
                }
              } catch (Exception e) {
                throw new OLATRuntimeException(Window.class,renderResult.getLogMsg(), renderResult.getRenderException());
              } finally {
                toRender.setDomReplaceable(true);
              }
              if (renderResult.getRenderException() != null) throw new OLATRuntimeException(Window.class, renderResult.getLogMsg(),
                  renderResult.getRenderException());
             
              AsyncMediaResponsible curAmr = renderResult.getAsyncMediaResponsible();
              if (curAmr != null) {
                if (amr != null) {
                  throw new AssertException("can set amr only once in a screen!");
                } else {
                  amr = curAmr;
                }
              }
             
              JSONObject jo = new JSONObject();
              long cid = toRender.getDispatchID();
              if (Settings.isDebuging()) {
                // for debugging only
                jo.put("cname", toRender.getComponentName());
                jo.put("clisteners",toRender.getListenerInfo());
                jo.put("hfragsize", result.length());
              }           
             
              jo.put("cid", cid);
              jo.put("cidvis", toRender.isVisible());
              jo.put("hfrag", result);
              jo.put("jsol", jsol);
              jo.put("hdr", hdr);
              ja.put(jo);
            }
            //polling case should never set the asyncMediaResp.
            //to null otherwise it possible that e.g. pdf served as following click within a CP component
            if (amr != null) setAsyncMediaResponsible(amr);
                     
            if (isDebugLog) {
              long rstop = System.currentTimeMillis();
              debugMsg.append(";inl_part_render:").append((rstop-rstart));
              Tracing.logDebug(debugMsg.toString(), Window.class);
            }
          }
          com = CommandFactory.createDirtyComponentsCommand();
          com.setSubJSON(root);
          return com;
         
        } catch (JSONException e) {
          throw new AssertException("wrong data put into json object", e);
        }
      } 
      return com;
    }
  }

  /**
   * builds a url for this window
   *
   * @param win the window id the new url
   * @param timestampId
   * @param componentId
   * @param moduleUri
   * @param bc the businesscontrolpath
   * @return the new (relative) url as a string
   */
  private String buildURIFor(Window win, String timestampId, String moduleUri) {
    URLBuilder ubu = new URLBuilder(uriPrefix, win.getInstanceId(), timestampId, wbackofficeImpl);
    StringOutput so = new StringOutput();
    ubu.buildURI(so, null, null, moduleUri, 0);
    String uri = so.toString();
    return uri;
 

  private String buildURIForRedirect(String moduleUri) {
    return buildURIFor(this, "-1", moduleUri);
  }
 
  private String buildRenderOnlyURIFor(Window win) {
    return buildURIFor(win, null, null);
  }
 
  /**
   * @param ureq
   * @return true if the event was indeed dispatched to the component, false
   *         otherwise (reasons: no component found with given id, or component
   *         disabled)
   */
  private boolean doDispatchToComponent(UserRequest ureq, StringBuilder debugMsg) {
    String s_compID = ureq.getComponentID();
    if (s_compID == null) return false; //throw new AssertException("no component id found in req:" + ureq.toString());
   
   
    Component target;
    List<Component> foundPath = new ArrayList<Component>(10);
   
    // OLAT-1973
    if (GUIInterna.isLoadPerformanceMode()) {
      String compPath = ureq.getParameter("e");
      Component cur = getContentPane();
      String[] res = compPath.split("!");
      boolean correctFullPath = true;
      for (int i = res.length -1; i >= 0 && correctFullPath; i--) {
        String cname = res[i]
        Container co = (Container) cur; // we did not record the leaf, so we know it's a container
        Component c = co.getComponent(cname);
        if (c == null) {
          correctFullPath = false;
          //throw new AssertException("cannot find: "+compPath);
        } else {
          foundPath.add(c);
          cur = c;
        }
      }
      // if we could not find our component following the full path, also search the component in the full component tree.
      // the reason is that simply adding a panel or such around a component should not break existing (jmeter)-functional-tests.
      //
      // As long as we find only one component with a child with the name we search, we are ok and assume this new component to be the
      // same from a gui-side meaning, even if it is in a different parent hierarchy.
      // If more than one match is found (which should occur very rarely when developers choose meaningful names for components
      // to be put into containers), we cannot determine which component was meant and must throw an exception - the functional test
      // will break and needs to be improved/adjusted to the new layout.
      // if no match is found, we could not find the component at all and must raise an exception also.
      //
      if (correctFullPath) {
        // cur is now the component to dispatch
        target = cur;
      } else {
        String childName = res[0]; // Pre: all paths have at least one entry 
        List<Component> founds = ReusableURLHelper.findComponentsWithChildName(childName, getContentPane());
        int foundsCnt = founds.size();
        if (foundsCnt == 1) {
          // unique -> high probability that the recorded link is still the same
          target = founds.get(0);
        } else if (foundsCnt == 0) {
          throw new AssertException("cannot find: "+compPath);
        } else { // >1 -> ambiguous, two possible targets
          throw new AssertException("cannot find: "+compPath);
        }
      }     
    } else {
      long compID = Long.parseLong(s_compID); // throws NumberFormatException if
      // not a number
      target = ComponentHelper.findDescendantOrSelfByID(getContentPane(), compID, foundPath);     
    }
   
    if (target == null) {
      // there was a component id given, but no matching target could be found
      fireEvent(ureq, COMPONENTNOTFOUND);
      return false;
      // do not dispatch; which means just rerender later; good if
      // the
      // gui tree was changed by another thread in the meantime.
      // do not throw an exception here, because this can happen if the gui
      // tree was changed by another thread in the meantime
    }
    if (!target.isVisible()) { throw new OLATRuntimeException(Window.class, "target with name: '" + target.getComponentName()
        + "', was invisible, but called to dispatch", null); }
    boolean toDispatch = true; //TODO:fj:c is foundpath needed for something else than the enabled-check. if no -> one boolean is enough
    for (Iterator iter = foundPath.iterator(); iter.hasNext();) {
      Component curComp = (Component) iter.next();
      if (!curComp.isEnabled()) {
        toDispatch = false;
        break;
      }
    }
    if (toDispatch) {
      latestDispatchComponentInfo = target.getComponentName() + " :" + target.getExtendedDebugInfo();
      latestDispatchedComp = target;
      Codepoint.setThreadLocalLogDetails(latestDispatchComponentInfo);
     
      // dispatch
      wbackofficeImpl.fireCycleEvent(Window.ABOUT_TO_DISPATCH);
      target.dispatchRequest(ureq);
     
      // after dispatching, commit (docu)
      DBFactory.getInstance().commit();
     
      // add the new URL to the browser history, but not if the klick resulted in a new browser window (a href .. target=...) or in a download (excel or such)
      DispatchResult dr = ureq.getDispatchResult();
      MediaResource tMr = dr.getResultingMediaResource();
      Window tWin = dr.getResultingWindow();
      if (tMr == null && tWin == null) {
        addToHistory(new HistoryEntry(ureq));
      }
      wbackofficeImpl.fireCycleEvent(END_OF_DISPATCH_CYCLE);
     
     
      // if loglevel is set accordingly, collect anonymous controller usage statistics.
      if (debugMsg != null) {
        Controller c = target.getLatestDispatchedController();
        if (c != null) {
          WindowControl wCo = null;
          try {
            wCo = c.getWindowControlForDebug();
          } catch (Exception e) {
            // getWindowControl throw an Assertion if wControl = null
          }
          if (wCo != null) {
            String coInfo = "";
            WindowControlInfo wci = wCo.getWindowControlInfo();
            while (wci != null) {
              String cName = wci.getControllerClassName();
              coInfo = cName + ":" + coInfo; 
              wci = wci.getParentWindowControlInfo();
            }
           
            BusinessControl bc = wCo.getBusinessControl();
            String businessPath = bc == null? "n/a":bc.getAsString();
            String compName = target.getComponentName();
            String msg = "wci:"+coInfo+"%%"+compName+"%%"+businessPath+"%%";
            // allowed for debugging, dispatching is already over
            Event ev = target.getAndClearLatestFiredEvent();
            if (ev != null) {
              msg += ev.getClass().getName()+":"+ev.getCommand()+"%%";
            }
            String targetInfo = target.getExtendedDebugInfo();
            msg += targetInfo+"%%";
            debugMsg.append(msg).append(LOG_SEPARATOR);
            //Tracing.logDebug(msg, WindowStats.class);
          } else {
            // no windowcontrol -> ignore           
          }
        } // else: a component with -no- controller as listener, makes no sense in 99.99% of the cases; ignore in those rare cases
      } else {
        // no debug level, consume the left over event (for minor memory reasons)
        target.getAndClearLatestFiredEvent();
      }
     
      // we do not want to keep a reference which could be old.
      // in case we do not reach the next line because of an exception in dispatch(), we clear the value in the exceptionwindowcontroller's error handling     
      latestDispatchedComp = null;
    }
    return toDispatch;
  }


  /**
   * Sets the asyncMediaResponsible.
   *
   * @param asyncMediaResponsible The asyncMediaResponsible to set
   */
  private void setAsyncMediaResponsible(AsyncMediaResponsible asyncMediaResponsible) {
    this.asyncMediaResponsible = asyncMediaResponsible;
  }

  /**
   * @return String
   */
  public String getInstanceId() {
    return instanceId;
  }

  /**
   * Sets the instanceId.
   *
   * @param instanceId The instanceId to set
   */
  public void setInstanceId(String instanceId) {
    this.instanceId = instanceId;
  }

  /**
   * Sets the uriPrefix.
   *
   * @param uriPrefix The uriPrefix to set
   */
  public void setUriPrefix(String uriPrefix) {
    this.uriPrefix = uriPrefix;
  }

  /**
   * @return LatestDispatchComponentInfo
   */
  public String getLatestDispatchComponentInfo() {
    return latestDispatchComponentInfo;
  }

  /**
   * @return the chiefcontroller that owns this window
   */
  /*public ChiefController getChiefController() {
    return chiefController;
  }*/

  public ComponentRenderer getHTMLRendererSingleton() {
    throw new AssertException("a window should never be rendered, but its contentpane");
  }

  /**
   * to be used for exception reporting only!
   * @return
   */
  public Component getAndClearLatestDispatchedComponent() {
    Component tmp = latestDispatchedComp;
    latestDispatchedComp = null;
    return tmp;
  }

}

class ValidatingVisitor implements ComponentVisitor {
  private ValidationResult validationResult;

  /**
   *
   */
  public ValidatingVisitor(GlobalSettings globalSetting, JSAndCSSAdder jsAndCSSAdder) {
    validationResult = new ValidationResult(globalSetting, jsAndCSSAdder);
  }

  /**
   * @see org.olat.core.util.component.ComponentVisitor#visit(org.olat.core.gui.components.Component,
   *      org.olat.core.gui.UserRequest)
   */
  public boolean visit(Component comp, UserRequest ureq) {
    // validate only visble components
    if (comp.isVisible()) {
      comp.validate(ureq, validationResult);
      return true;
    }
    return false;
  }

  /**
   * @return the validationresult
   */
  ValidationResult getValidationResult() {
    return validationResult;
  }
}
TOP

Related Classes of org.olat.core.gui.components.ValidatingVisitor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.