/** * Copyright 2010 Google Inc. * * Licensed 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.wave.client.wavepanel.view; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import org.waveprotocol.wave.client.common.util.StringCodec; import org.waveprotocol.wave.model.conversation.Conversation; import org.waveprotocol.wave.model.conversation.ConversationBlip; import org.waveprotocol.wave.model.conversation.ConversationThread; import org.waveprotocol.wave.model.conversation.ConversationView; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.Pair; import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.wave.ParticipantId; /** * Provides an isomorphic mapping between model objects and strings. * */ public final class ModelIdMapperImpl implements ModelIdMapper { // Because of the loosely defined alphabets of input strings (conversation // ids, blip ids, participant ids), this code assumes that all characters are // permitted in those strings. This means that constructing product values // (pairs, triples etc) must be done with linear-cost quoting. private final static StringCodec CODEC = StringCodec.INSTANCE; /** Component separator, as provided by StringCodec. */ private final static String SEP = CODEC.free().substring(0, 1); /** Model context in which this mapper is mapping. */ private final ConversationView model; /** Prefix for short ids, used for namespacing. */ private final String prefix; /** Counter for the next short id. */ private int nextId; /** * 2 String map that maps between longId and shortId. * There are 2 invariants that hold * longIdToShortIdMap.containKey(longId) -> * shortIdToLongIdMap.get(longIdToShortIdMap.get(longId)).equals(longId) * * shortIdToLongIdMap.containKey(shortId) -> * longIdToShortIdMap.get(shortIdToLongIdMap.get(shortId)).equals(shortId) */ private final StringMap<String> shortIdToLongIdMap = CollectionUtils.createStringMap(); private final StringMap<String> longIdToShortIdMap = CollectionUtils.createStringMap(); @VisibleForTesting ModelIdMapperImpl(ConversationView model, String prefix, int nextId) { this.model = model; this.prefix = prefix; this.nextId = nextId; } /** * Creates a new mapper. * * @param model model universe * @param prefix prefix on all short ids, for namespacing */ public static ModelIdMapperImpl create(ConversationView model, String prefix) { return new ModelIdMapperImpl(model, prefix, 0); } /** * Creates a new mapper. * * @param model model universe * @param prefix prefix on all short ids, for namespacing * @param nextId initial counter for id generation */ public static ModelIdMapperImpl create(ConversationView model, String prefix, int nextId) { return new ModelIdMapperImpl(model, prefix, nextId); } @Override public String conversationsId(ConversationView cs) { Preconditions.checkArgument(model.equals(cs)); return prefix; } @Override public String convId(Conversation c) { return shorten(c.getId()); } @Override public String participantId(Conversation c, ParticipantId p) { return shorten(longConvId(c) + SEP + CODEC.encode(p.getAddress())); } @Override public String threadId(ConversationThread t) { return shorten(longConvId(t.getConversation()) + SEP + CODEC.encode(t.getId())); } @Override public String blipId(ConversationBlip b) { return shorten(longConvId(b.getConversation()) + SEP + CODEC.encode(b.getId())); } private String longConvId(Conversation c) { return CODEC.encode(c.getId()); } @Override public ConversationBlip locateBlip(String modelId) { String longId = restoreId(modelId); String[] parts = split(longId); if (parts.length != 2) { throw new IllegalArgumentException("Not a blip model id: " + modelId); } else { Conversation c = model.getConversation(parts[0]); return c != null ? c.getBlip(parts[1]) : null; } } @Override public ConversationThread locateThread(String modelId) { String longId = restoreId(modelId); String[] parts = split(longId); if (parts.length != 2) { throw new IllegalArgumentException("Not a thread model id: " + modelId); } else { Conversation c = model.getConversation(parts[0]); return c != null ? c.getThread(parts[1]) : null; } } @Override public Conversation locateConversation(String modelId) { return model.getConversation(restoreId(modelId)); } @Override public Pair<Conversation, ParticipantId> locateParticipant(String modelId) { String longId = restoreId(modelId); String[] parts = split(longId); if (parts.length != 2) { throw new IllegalArgumentException("Not a participant model id: " + modelId); } else { Conversation c = model.getConversation(parts[0]); ParticipantId p = new ParticipantId(parts[1]); return Pair.of(c, p); } } private static String[] split(String domId) { // Split with -1, rather than 0, to preserve empty strings, which are // legitimated model ids (e.g., the root thread of a conversation has an // empty id). String [] parts = domId.split(SEP, -1); for (int i = 0; i < parts.length; i++) { parts[i] = CODEC.decode(parts[i]); } return parts; } /** * Shortens a long id into a short id. If there is already a mapping for * {@code longId}, the existing short id is returned. Otherwise, a new short * id is generated and returned. */ @VisibleForTesting public String shorten(String longId) { String shortId = longIdToShortIdMap.get(longId); if (shortId == null) { shortId = prefix + nextId++; registerIdPair(shortId, longId); } return shortId; } /** * Restores a shortId back to a longId. */ @VisibleForTesting public String restoreId(String shortId) { return shortIdToLongIdMap.get(shortId); } /** * Registers an id pair. * * @param shortId short id (arbitrary) * @param longId long id (must conform to the grammar for model ids). */ public void registerIdPair(String shortId, String longId) { Preconditions.checkArgument(!shortIdToLongIdMap.containsKey(shortId) && !longIdToShortIdMap.containsKey(longId), "Id already exist"); shortIdToLongIdMap.put(shortId, longId); longIdToShortIdMap.put(longId, shortId); } /** * Applies a callback to each pair (shortId, longId) in the mapping. * * @param callback callback to invoke on each pair. */ public void forEachPair(ProcV<? super String> callback) { shortIdToLongIdMap.each(callback); } /** @return the counter to use for the next short id. */ public int getNextId() { return nextId; } /** @return the prefix to prepend on the next short id. */ public String getPrefix() { return prefix; } }