Package com.tll.client.mvc

Source Code of com.tll.client.mvc.ViewManager$ViewChangeHandlers

/**
* The Logic Lab
* @author jpk Jan 3, 2008
*/
package com.tll.client.mvc;

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

import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.Panel;
import com.tll.client.model.IModelChangeHandler;
import com.tll.client.model.ModelChangeEvent;
import com.tll.client.mvc.view.IHasViewChangeHandlers;
import com.tll.client.mvc.view.IModelAwareView;
import com.tll.client.mvc.view.IView;
import com.tll.client.mvc.view.IViewChangeHandler;
import com.tll.client.mvc.view.IViewInitializer;
import com.tll.client.mvc.view.IViewRequest;
import com.tll.client.mvc.view.ViewChangeEvent;
import com.tll.client.mvc.view.ViewKey;
import com.tll.client.mvc.view.ViewOptions;
import com.tll.client.mvc.view.ViewRef;
import com.tll.client.ui.view.ViewContainer;

/**
* ViewManager - Singleton managing view life-cycles and view caching. Also
* serves as an MVC dispatcher dispatching view requests to the appropriate view
* controller. View history is also managed here.
* <p>
* <b>NOTE:</b> we use the double type rather than the int type when dealing
* with view key hash codes to avoid js exceptions when parsing history view
* tokens in script mode!
* @author jpk
*/
public final class ViewManager implements ValueChangeHandler<String>, IHasViewChangeHandlers, IModelChangeHandler {

  /**
   * ViewChangeHandlers
   * @author jpk
   */
  @SuppressWarnings("serial")
  static final class ViewChangeHandlers extends ArrayList<IViewChangeHandler> {

    public void fireEvent(ViewChangeEvent event) {
      for(final IViewChangeHandler handler : this) {
        handler.onViewChange(event);
      }
    }
  }

  /**
   * Creates a {@link ViewRef} given a {@link CView}.
   * @param e the view
   * @return Newly created {@link ViewRef}.
   */
  private static ViewRef ref(CView e) {
    return new ViewRef(e.init, e.vc.getView().getShortViewName(), e.vc.getView().getLongViewName());
  }

  /**
   * Used to dis-ambiguate {@link History} tokens. I.e. whether the history
   * token passed to the {@link History} {@link ValueChangeEvent} method is a
   * call for a view.
   */
  private static final char VIEW_TOKEN_PREFIX = 'v';

  /**
   * Generates a history token from a {@link ViewKey}.
   * @param key
   * @return the complimenting history token.
   */
  private static String generateViewKeyHistoryToken(ViewKey key) {
    return VIEW_TOKEN_PREFIX + Double.toString(key.hashCode());
  }

  /**
   * Extracts the view key hash from the given history token. <code>-1</code> is
   * returned if the historyToken is <em>not</em> view related.
   * @param historyToken The possibly view related history token
   * @return Extracted hash of the associated {@link ViewKey} or <code>-1</code>
   *         if the history token is not a view history token.
   */
  @SuppressWarnings("null")
  private static double extractViewKeyHash(String historyToken) {
    final int len = historyToken == null ? 0 : historyToken.length();
    if(len < 2) return -1;
    if(historyToken.charAt(0) != VIEW_TOKEN_PREFIX) return -1;
    return Double.parseDouble(historyToken.substring(1));
  }

  private static ViewManager instance;

  /**
   * Must be called once by the app on startup.
   * @param parentViewPanel The Panel that is the parent of the pinned view
   *        container. Must not be <code>null</code>.
   * @param cacheCapacity The limit of the number of views to hold in memory.
   */
  public static void initialize(Panel parentViewPanel, int cacheCapacity) {
    instance = new ViewManager(parentViewPanel, cacheCapacity);
    // ModelChangeDispatcher.get().addModelChangeHandler(instance);
  }

  /**
   * Clears and nullifies the singleton instance.
   */
  public static void shutdown() {
    if(instance != null) {
      // ModelChangeDispatcher.get().removeModelChangeHandler(instance);
      instance.clear();
      instance = null;
    }
  }

  /**
   * @return The singleton {@link ViewManager} instance.
   */
  public static ViewManager get() {
    if(instance == null) throw new IllegalStateException("Not initialized.");
    return instance;
  }

  /**
   * The parent view panel. This property must be set so that views can attach
   * to the DOM.
   */
  private final Panel parentViewPanel;

  /**
   * The view cache.
   */
  private final ViewCache cache;

  /**
   * The first and currently pinned view.
   */
  private CView initial, current, pendingUnload;

  /**
   * The controllers to handle view requests.
   */
  private final List<IController> controllers = new ArrayList<IController>();

  /**
   * The collection of view change listeners that are notified when the current
   * view changes.
   */
  private final ViewChangeHandlers viewChangeHandlers = new ViewChangeHandlers();

  /**
   * The view request that is pending.
   * <p>
   * In order to comply with the history event system, we must be routed from
   * the browser history context.
   * <p>
   * <strong>view request procedure:</strong>
   * <ol>
   * <li>Dispatch a view request via {@link #dispatch(IViewRequest)}.
   * <li>Temporarily cache the view request in {@link #pendingViewRequest}.
   * <li>Invoke {@link History#newItem(String)}
   * <li>Re-acquire the view request held in {@link #pendingViewRequest}.
   * <li>Dispatch normally
   * </ol>
   */
  private IViewRequest pendingViewRequest;

  /**
   * Constructor
   * @param parentPanel The panel that will contained pinned views.
   * @param cacheCapacity
   * @see ViewManager#initialize(Panel, int)
   */
  private ViewManager(Panel parentPanel, int cacheCapacity) {
    parentViewPanel = parentPanel;
    cache = new ViewCache(cacheCapacity);
    History.addValueChangeHandler(this);

    // add supported controllers
    controllers.add(new ShowViewController());
    controllers.add(new UnloadViewController());
    controllers.add(new PinPopViewController());
  }

  @Override
  public void addViewChangeHandler(IViewChangeHandler handler) {
    viewChangeHandlers.add(handler);
  }

  @Override
  public void removeViewChangeHandler(IViewChangeHandler handler) {
    viewChangeHandlers.remove(handler);
  }

  /**
   * @return The assigned panel to which views are shown.
   */
  public Panel getParentViewPanel() {
    return parentViewPanel;
  }

  /**
   * Sets the current view. The current view is defined as the visible pinned
   * view.
   * @param init The view initializer employed only when the view is not present
   *        in the view cache
   */
  @SuppressWarnings("unchecked")
  void setCurrentView(IViewInitializer init) {
    final ViewKey key = init.getViewKey();
    Log.debug("Setting current view: '" + key + "' ..");

    CView e;
    final ViewOptions options = init.getViewKey().getViewClass().getViewOptions();
    final int cacheIndex = cache.searchQueue(key);
    final boolean showPopped = ((cacheIndex == -1) && options.isInitiallyPopped());

    if(cacheIndex != -1) {
      // existing cached view
      e = cache.removeAt(cacheIndex);
      assert e != null;
      setCurrentView(e, showPopped);
    }
    else {
      Log.debug("Creating and initializing view: " + key + " ..");
      // non-cached view
      final IView<IViewInitializer> view = (IView<IViewInitializer>) key.getViewClass().newView();

      // initialize the view
      view.initialize(init);

      e = new CView(new ViewContainer(view, options, key), init);
      setCurrentView(e, showPopped);

      view.apply(e.vc, e.vc.getToolbar());

      // load the view
      view.refresh();
    }
  }

  /**
   * Sets the current view given a presently cached view container.
   * @param e primary cache element
   * @param showPopped show popped or pinned?
   */
  private void setCurrentView(CView e, boolean showPopped) {
    final ViewContainer vc = e.vc;

    // set the view
    if(showPopped) {
      // NOTE: the view history is not affected!
      vc.pop(parentViewPanel);
    }
    else {
      final boolean sameView = (current != null && current.equals(e));
      final boolean pndgIsPopped = e.vc.isPopped();
      final boolean crntIsPopped = current != null && current.vc.isPopped();

      final boolean rmvCrnt = current != null && ((!sameView && !crntIsPopped) || (sameView && pndgIsPopped));
      final boolean pinPndg = (rmvCrnt || (sameView && crntIsPopped) || !sameView || !e.vc.isAttached());

      if(rmvCrnt) {
        // remove current pinned view
        current.vc.removeFromParent();
      }
      if(pinPndg) {
        // pin the view
        vc.pin(parentViewPanel);
      }
      // set as current
      current = e;
    }

    // add the view to the cache
    CView old = cache.cache(e);
    if(old != null) {
      assert old != e && !old.getViewKey().equals(e.getViewKey());
      Log.debug("Destroying view - " + old.vc.getView().toString() + "..");
      // view life-cycle destroy
      old.vc.getView().onDestroy();
      old = null;
    }

    // unload pending cview
    if(pendingUnload != null) {
      Log.debug("Destroying pending unload view- " + pendingUnload.vc.getView().toString() + "..");
      pendingUnload.vc.getView().onDestroy();
      pendingUnload = null;
    }

    // set the initial view if not set
    if(initial == null) initial = e;

    // fire view changed event
    fireViewChangeEvent();
  }

  /**
   * Fires a view change event to all subscribed handlers.
   */
  private void fireViewChangeEvent() {
    DeferredCommand.addCommand(new Command() {

      @SuppressWarnings("synthetic-access")
      @Override
      public void execute() {
        viewChangeHandlers.fireEvent(new ViewChangeEvent());
      }
    });
  }

  /**
   * Unloads the given view optionally removing it from the view cache then
   * updates the current pinned view routing through the history system.
   * @param key The view key of the view to unload
   * @param destroy Remove the view from the queue of current views?
   * @param erradicate Physically remove the view from cache entirely?
   */
  void unloadView(ViewKey key, boolean destroy, boolean erradicate) {
    final CView e = findView(key);
    if(e == null) return;

    // unload the given view
    e.vc.close();

    if(destroy) {
      pendingUnload = e;
      // remove the view from cache
      cache.remove(key);
    }
    if(erradicate) {
      cache.removeFromStack(key);
    }

    // find the newest pinned view excluding the one to be unloaded
    CView pendingCurrent = findFirstView(e);
    if(pendingCurrent == null && e != initial) {
      pendingCurrent = initial;
    }
    if(pendingCurrent != null && pendingCurrent != current) {
      History.newItem(generateViewKeyHistoryToken(pendingCurrent.getViewKey()));
    }
    else {
      // we're unloading a view that isn't current but we still need to notify
      // our view listeners so they remain in sync
      if(pendingUnload != null) {
        pendingUnload.vc.getView().onDestroy();
        pendingUnload = null;
      }
      fireViewChangeEvent();
    }
  }

  /**
   * Removes all view artifacts from the DOM and clears the cache.
   */
  public void clear() {
    Log.debug("Clearing view cache..");
    if(cache.size() > 0) {
      for(final Iterator<CView> itr = cache.queueIterator(); itr.hasNext();) {
        final CView e = itr.next();
        e.vc.close();
        e.vc.getView().onDestroy();
      }
    }
    cache.clear();
    Log.debug("View cache cleared");
  }

  /**
   * Generic find view method returning the first found match in the view cache.
   * @param exclude The view to exclude from the search. May be
   *        <code>null</code>.
   * @return The first found view or <code>null</code> if no match found.
   */
  private CView findFirstView(CView exclude) {
    final Iterator<CView> itr = cache.queueIterator();
    if(itr != null) {
      while(itr.hasNext()) {
        final CView e = itr.next();
        if(exclude == null || exclude != e) {
          return e;
        }
      }
    }
    return null;
  }

  /**
   * Locates a cached view given a view key.
   * @param key The view key
   * @return The found {@link IView} or <code>null</code> if not present in the
   *         view cache.
   */
  private CView findView(ViewKey key) {
    final Iterator<CView> itr = cache.queueIterator();
    if(itr != null) {
      while(itr.hasNext()) {
        final CView e = itr.next();
        if(e.getViewKey().equals(key)) {
          return e;
        }
      }
    }
    return null;
  }

  /**
   * Locates a cached view given a view key hash.
   * @param viewKeyHash
   * @return The found {@link IView} or <code>null</code> if not present in the
   *         view cache.
   */
  private CView findView(double viewKeyHash) {
    final Iterator<CView> itr = cache.queueIterator();
    if(itr != null) {
      while(itr.hasNext()) {
        final CView e = itr.next();
        final double hc = e.getViewKey().hashCode();
        if(hc == viewKeyHash) {
          return e;
        }
      }
    }
    return null;
  }

  /**
   * Resolves a view key hash to the associated view ref held in the view cache.
   * <p>
   * <b>NOTE:</b> we could throw an exception here since we are retaining view
   * refs for *all* visited views and, therefore, we expect to be able to
   * resolve any view key hash to a view ref but we account for possibility the
   * user may have manually changed the view key hash in the query string or
   * equivalent.
   * @param viewKeyHash
   * @return The found view ref or <code>null</code> if not found.
   */
  private ViewRef findViewRef(double viewKeyHash) {
    // try visited view ref cache
    final Iterator<ViewRef> vitr = cache.visitedRefIterator();
    if(vitr != null) {
      while(vitr.hasNext()) {
        final ViewRef r = vitr.next();
        final int hc = r.getViewKey().hashCode();
        if(hc == viewKeyHash) {
          return r;
        }
      }
    }
    return null;
  }

  /**
   * @return Never <code>null</code> array of the currently cached views from
   *         most recently visited (head) to oldest (tail) which may be empty
   *         indicating there are currently no cached views.
   */
  /*
  public IView<?>[] getCachedViews() {
    if(cache.size() == 0) {
      return new IView[] {};
    }
    final ArrayList<IView<?>> list = new ArrayList<IView<?>>(cache.size());
    for(final Iterator<CView> itr = cache.queueIterator(); itr.hasNext();) {
      list.add(itr.next().vc.getView());
    }
    return list.toArray(new IView[list.size()]);
  }
   */

  /**
   * Provides an array of the cached views as stand-alone references in "cache"
   * order (head is newest).
   * <p>
   * <b>IMPT:</b> The current view is <em>not</em> included.
   * @param capacity the maximum number of view refs to provide in the returned
   *        array. <code>-1</code> indicates un-bounded in which case the
   *        capacity is that of the number of distinct views visited.
   * @param includePopped Included views that are currently popped?
   * @param includeFirst Include the first cached view? <br>
   *        NOTE: This view is <em>always</em> retained.
   * @return A newly created never <code>null</code> array of view references.
   */
  public ViewRef[] getViewRefs(int capacity, boolean includePopped, boolean includeFirst) {
    if(initial == null) return new ViewRef[0];
    assert current != null;

    if(capacity == -1) capacity = cache.numVisited();

    int count = 0;

    final ArrayList<ViewRef> plist = new ArrayList<ViewRef>();

    final Iterator<ViewRef> ritr = cache.visitedRefIterator();
    if(ritr != null) {
      while(ritr.hasNext() && count < capacity) {
        ViewRef r = ritr.next();
        if(current.compareTo(r) == 0) {
          // don't include the current view
          r = null;
        }
        else if(!includePopped) {
          final CView e = cache.peekQueue(r.getViewKey());
          if(e != null && e.vc.isPopped()) {
            r = null;
          }
        }
        if(r != null) {
          plist.add(r);
          count++;
        }
      }
    }
    assert count <= capacity;

    // include the initial view if called for and not already in list and not
    // the current view
    if(includeFirst && !initial.equals(current)) {
      // verify not already present
      int initialIndex = -1;
      for(int i = 0; i < plist.size(); i++) {
        if(initial.compareTo(plist.get(i)) == 0) {
          initialIndex = i;
          break;
        }
      }
      if(initialIndex == -1) {
        if(count == capacity) {
          plist.remove(plist.size() - 1);
        }
        plist.add(ref(initial));
      }
    }

    return plist.toArray(new ViewRef[plist.size()]);
  }

  /**
   * Pops the currently visible pinned view out of the natural flow of the DOM
   * document routing the {@link ViewKey} of the view that is to be the
   * subsequent pinned view through the browser history system.
   */
  void popCurrentView() {
    if(current != null) {
      assert !current.vc.isPopped();

      // pop the view
      current.vc.pop(parentViewPanel);

      final CView nextCurrent = findFirstView(current);
      final ViewKey vk = nextCurrent == null ? null : nextCurrent.getViewKey();
      if(vk != null) {
        History.newItem(generateViewKeyHistoryToken(vk));
      }
    }
  }

  /**
   * Pins a popped view.
   * @param key the view key of the popped view to pin
   */
  void pinPoppedView(ViewKey key) {
    final CView e = findView(key);
    if(e == null) throw new IllegalStateException();
    if(e.vc.isPopped()) {
      setCurrentView(e, false);
    }
  }

  /**
   * Dispatches the view request event to the appropriate controller.
   * @param request The view request
   */
  public void dispatch(IViewRequest request) {
    if(request == null) throw new IllegalArgumentException("No view request specified.");

    if(pendingViewRequest == null) {
      if(request.addHistory()) {
        // history routing required
        assert request.getViewKey() != null : "Unable to add history: No view key specified.";
        final double hash = extractViewKeyHash(History.getToken());
        final double vkhash = request.getViewKey().hashCode();
        if(hash != -1 && vkhash == hash) {
          doDispatch(request);
        }
        else {
          // need to route through history first
          this.pendingViewRequest = request;
          final String htoken = generateViewKeyHistoryToken(request.getViewKey());
          Log.debug("Routing view '" + request.getViewKey() + "' through history with hash: " + htoken);
          History.newItem(htoken);
        }
      }
      else {
        // no history routing required
        doDispatch(request);
      }
    }
    else {
      assert request == pendingViewRequest;
      pendingViewRequest = null; // reset
      doDispatch(request);
    }
  }

  private void doDispatch(IViewRequest request) {
    // do actual disptach
    Log.debug("Dispatching view request: " + request + " ..");
    for(final IController c : controllers) {
      if(c.canHandle(request)) {
        c.handle(request);
        return;
      }
    }
    throw new IllegalStateException("Unhandled view request: " + request);
  }

  public void onValueChange(ValueChangeEvent<String> event) {
    final double viewKeyHash = extractViewKeyHash(event.getValue());
    if(viewKeyHash != -1) {
      Log.debug("Handling view history token: " + viewKeyHash + "..");
      if(pendingViewRequest != null) {
        // dispatch the view request
        dispatch(pendingViewRequest);
      }
      else {
        // user pressed the back button or a non-show type view request was invoked or equivalant
        CView e = findView(viewKeyHash);
        if(e == null) {
          // probably the user is clicking the back button a number of times beyond the cache capacity
          // resort to the visited view ref cache
          final ViewRef r = findViewRef(viewKeyHash);
          if(r == null) {
            Log.warn("Un-resolved view hash: " + viewKeyHash);
          }
          else {
            setCurrentView(r.getViewInitializer());
            return;
          }
          // this should only happen when the user mucks with the view key hash in the query string
          // resort to the initial view
          e = initial;
        }
        if(e != null) {
          setCurrentView(e, false);
        }
        else {
          Log.debug("Un-resolvable view hash: " + viewKeyHash + ". No action performed.");
        }
      }
    }
  }

  @Override
  public void onModelChangeEvent(ModelChangeEvent event) {
    // apply the model change to the currently cached views
    for(final Iterator<CView> itr = cache.queueIterator(); itr.hasNext();) {
      final IView<?> view = itr.next().vc.getView();
      if(view instanceof IModelAwareView<?>) {
        ((IModelAwareView<?>) view).onModelChangeEvent(event);
      }
    }
  }
}
TOP

Related Classes of com.tll.client.mvc.ViewManager$ViewChangeHandlers

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.