Package org.waveprotocol.box.webclient.search

Source Code of org.waveprotocol.box.webclient.search.SimpleSearch$DigestProxy

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.waveprotocol.box.webclient.search;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.gwt.http.client.Request;

import org.waveprotocol.box.webclient.search.SearchService.Callback;
import org.waveprotocol.box.webclient.search.SearchService.DigestSnapshot;
import org.waveprotocol.wave.client.debug.logger.DomLogger;
import org.waveprotocol.wave.common.logging.LoggerBundle;
import org.waveprotocol.wave.model.document.WaveContext;
import org.waveprotocol.wave.model.id.ModernIdSerialiser;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.CopyOnWriteSet;
import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.wave.ParticipantId;

import java.util.List;

/**
* A simple implementation of the search model, using a search service.
* <p>
* This search keeps a list that corresponds to the total search result size.
* Segments of that list are filled in as necessary.
*
* @author hearnden@google.com (David Hearnden)
*/
public final class SimpleSearch implements Search, WaveStore.Listener {

  private final static LoggerBundle log = new DomLogger("search");

  /**
   * Wraps a digest snapshot, but can switch to an optimistic wave-based digest
   * if a wave is available ({@link #activate} and {@link #deactivate}). The
   * snapshot this proxy wraps can also be replaced, e.g., from updated search
   * results, with {@link #update}. This proxy supports liveness, notifying
   * listeners of changes.
   */
  class DigestProxy implements Digest, WaveBasedDigest.Listener {
    /** Snapshot from the search result. Never null. */
    private DigestSnapshot staticDigest;
    /** Optimistic digest from a wave. May be null. */
    private WaveBasedDigest dynamicDigest;

    DigestProxy(DigestSnapshot staticDigest) {
      Preconditions.checkArgument(staticDigest != null);
      this.staticDigest = staticDigest;
    }

    /**
     * Destroys this object, releasing its resources.
     */
    void destroy() {
      if (dynamicDigest != null) {
        deactivate();
      }
    }

    /**
     * Switches to a live digest, sourced from a wave. This fires a change
     * event.
     */
    void activate(WaveContext wave) {
      Preconditions.checkState(dynamicDigest == null);
      dynamicDigest = WaveBasedDigest.create(wave);
      dynamicDigest.addListener(this);
      fireOnChanged();
    }

    /**
     * Abandons the live digest, falling back to a static digest. The state from
     * the live digest is pushed into a static form, so this action should not
     * cause any change to the digest state, and so does not fire a change event.
     */
    void deactivate() {
      Preconditions.checkState(dynamicDigest != null);
      staticDigest =
          new DigestSnapshot(getTitle(), getSnippet(), getWaveId(), getAuthor(),
              getParticipantsSnippet(), getLastModifiedTime(), getUnreadCount(), getBlipCount());
      dynamicDigest.destroy();
      dynamicDigest = null;
    }

    /**
     * Updates the static digest. Do nothing if this digest is currently live.
     */
    void update(DigestSnapshot snapshot) {
      staticDigest = snapshot;
      if (dynamicDigest == null) {
        fireOnChanged();
      }
    }

    private Digest getDelegate() {
      return dynamicDigest != null ? dynamicDigest : staticDigest;
    }

    //
    // Forward Digest API to delegate.
    //

    @Override
    public WaveId getWaveId() {
      return getDelegate().getWaveId();
    }

    @Override
    public ParticipantId getAuthor() {
      return getDelegate().getAuthor();
    }

    @Override
    public List<ParticipantId> getParticipantsSnippet() {
      return getDelegate().getParticipantsSnippet();
    }

    @Override
    public String getTitle() {
      return getDelegate().getTitle();
    }

    @Override
    public String getSnippet() {
      return getDelegate().getSnippet();
    }

    @Override
    public int getUnreadCount() {
      return getDelegate().getUnreadCount();
    }

    @Override
    public int getBlipCount() {
      return getDelegate().getBlipCount();
    }

    @Override
    public double getLastModifiedTime() {
      return getDelegate().getLastModifiedTime();
    }

    //
    // Events.
    //

    @Override
    public void onChanged() {
      // Fan out events from the live digest to this digest's listeners.
      fireOnChanged();
    }

    private void fireOnChanged() {
      // TODO(hearnden): make not linear.
      fireOnDigestReady(results.indexOf(staticDigest), this);
    }
  }

  /** Service that performs searches. */
  private final SearchService searcher;

  /**
   * A list the size of the total search result, populated with digests that are
   * known to this search model.
   */
  private final List<DigestSnapshot> results = CollectionUtils.newArrayList();

  /**
   * Map of all digests.
   */
  private final StringMap<DigestProxy> digests = CollectionUtils.createStringMap();

  /** Store of all open waves in the client. */
  private final WaveStore waveStore;

  /** Listeners. */
  private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create();

  /** The request that is currently in flight, or {@code null}. */
  private Callback outstanding;

  /** Total size of the search result. */
  private int total = 0;

  private Request previousRequest;

  private String previousQuery;

  private int previousSize;

  @VisibleForTesting
  SimpleSearch(SearchService searcher, WaveStore store) {
    this.searcher = searcher;
    this.waveStore = store;
  }

  /**
   * Creates a search model.
   *
   * @param searcher service that performs searches
   * @param store store of open waves
   */
  public static SimpleSearch create(SearchService searcher, WaveStore store) {
    SimpleSearch search = new SimpleSearch(searcher, store);
    search.init();
    return search;
  }

  private void init() {
    waveStore.addListener(this);
  }

  /**
   * Destroys this search model, releasing its resources.
   */
  public void destroy() {
    destroyDigests();
    waveStore.removeListener(this);
    outstanding = null;
  }

  private void destroyDigests() {
    digests.each(new ProcV<DigestProxy>() {
      @Override
      public void apply(String key, DigestProxy value) {
        value.destroy();
      }
    });
    digests.clear();
    results.clear();
    total = 0;
  }

  @Override
  public void find(String query, int size) {
    if (previousRequest != null && previousRequest.isPending()) {
      if (query.equals(previousQuery) && size == previousSize) {
        // Same query, we should wait to the response
        return;
      }
    }
    previousQuery = query;
    previousSize = size;
    Callback callback = new Callback() {
      @Override
      public void onFailure(String message) {
        if (outstanding == this) {
          outstanding = null;
          previousRequest = null;
          handleFailure(message);
        }
      }

      @Override
      public void onSuccess(int total, List<DigestSnapshot> snapshots) {
        if (outstanding == this) {
          outstanding = null;
          previousRequest = null;
          handleSuccess(total, 0, snapshots);
        }
      }
    };

    if (outstanding == null) {
      outstanding = callback;
      previousRequest = searcher.search(query, 0, size, callback);
      fireOnStateChanged();
    } else {
      outstanding = callback;
      previousRequest = searcher.search(query, 0, size, callback);
    }
  }

  @Override
  public void cancel() {
    handleFailure("cancelled by user");
  }

  /**
   * Logs an error.  Destroys the current results.
   */
  private void handleFailure(String message) {
    log.error().log("Search failed: ", message);
    destroyDigests();
    fireOnStateChanged();
  }

  /**
   * Copies the digest snapshots into this search result's state.
   */
  private void handleSuccess(int total, int from, List<DigestSnapshot> newDigests) {
    if (this.total == total
        && from + newDigests.size() <= results.size()
        && results.subList(from, from + newDigests.size()).hashCode() == newDigests.hashCode()) {
      log.trace().log("handling vacuous update");
      // Assume no change, but notify listeners that the search is complete.
      fireOnStateChanged();
    } else {
      // For an incremental search, the result must be changed in steps that can
      // be communicated in the event language of the listener. Since the search
      // service is not incremental, computing a minimal diff is complicated.
      // Since the intent is eventually to make the search service itself
      // incremental, a brute force re-rendering here is a stop-gap.

      // Remove all digests.  Remove from last to first, so that remove is O(1).
      log.trace().log("handling changed search");
      for (int i = results.size() - 1; i >= 0; i--) {
        DigestSnapshot oldSnapshot = results.get(i);
        DigestProxy oldDigest = getDigest(i);
        results.remove(i);
        digests.remove(ModernIdSerialiser.INSTANCE.serialiseWaveId(oldDigest.getWaveId()));
        oldDigest.destroy();
      }
      // Now grow from nothing up to the new result size.
      if (this.total != total) {
        this.total = total;
      }
      ensureMinimumSize(total == Search.UNKNOWN_SIZE ? from + newDigests.size() : total);
      for (int to = from + newDigests.size(), i = from; i < to; i++) {
        results.set(i, newDigests.get(i - from));
      }
      fireOnTotalChanged(total);
      fireOnStateChanged();
    }
  }

  /**
   * Ensures that the result list is at least a certain size.
   */
  private void ensureMinimumSize(int total) {
    while (results.size() < total) {
      results.add(null);
    }
  }

  @Override
  public State getState() {
    return outstanding == null ? State.READY : State.SEARCHING;
  }

  @Override
  public DigestProxy getDigest(int index) {
    Preconditions.checkState(outstanding == null);
    DigestSnapshot result = results.get(index);
    WaveId waveId = result.getWaveId();
    String id = ModernIdSerialiser.INSTANCE.serialiseWaveId(waveId);
    DigestProxy proxy = digests.get(id);
    if (proxy == null) {
      proxy = new DigestProxy(result);
      digests.put(id, proxy);

      // Switch to a live digest if the wave is open.
      WaveContext wave = waveStore.getOpenWaves().get(waveId);
      if (wave != null) {
        proxy.activate(wave);
      }
    }

    return proxy;
  }

  @Override
  public int getTotal() {
    Preconditions.checkState(outstanding == null);
    return total;
  }

  @Override
  public int getMinimumTotal() {
    return results.size();
  }

  //
  // Events of interest to this search.
  //

  /**
   * If the opened wave is in the search result, switches to an optimistic digest.
   */
  @Override
  public void onOpened(WaveContext wave) {
    String id = ModernIdSerialiser.INSTANCE.serialiseWaveId(wave.getWave().getWaveId());
    DigestProxy digest = digests.get(id);
    if (digest != null) {
      log.trace().log("switching to active digest for: ", id);
      digest.activate(wave);
    }
  }

  @Override
  public void onClosed(WaveContext wave) {
    String id = ModernIdSerialiser.INSTANCE.serialiseWaveId(wave.getWave().getWaveId());
    DigestProxy digest = digests.get(id);
    if (digest != null) {
      log.trace().log("switching to passive digest for: ", id);
      digest.deactivate();
    }
  }

  //
  // Broadcast events.
  //

  @Override
  public void addListener(Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Listener listener) {
    listeners.remove(listener);
  }

  private void fireOnStateChanged() {
    for (Listener listener : listeners) {
      listener.onStateChanged();
    }
  }

  private void fireOnTotalChanged(int total) {
    for (Listener listener : listeners) {
      listener.onTotalChanged(total);
    }
  }

  private void fireOnDigestReady(int index, Digest digest) {
    for (Listener listener : listeners) {
      listener.onDigestReady(index, digest);
    }
  }

  private void fireOnDigestRemoved(int index, Digest digest) {
    for (Listener listener : listeners) {
      listener.onDigestRemoved(index, digest);
    }
  }
}
TOP

Related Classes of org.waveprotocol.box.webclient.search.SimpleSearch$DigestProxy

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.