// 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 com.google.enterprise.connector.instantiator; import com.google.common.collect.ImmutableMap; import com.google.enterprise.connector.logging.NDC; import com.google.enterprise.connector.persist.ConnectorStamps; import com.google.enterprise.connector.persist.PersistentStore; import com.google.enterprise.connector.persist.Stamp; import com.google.enterprise.connector.persist.StoreContext; import java.util.Iterator; import java.util.SortedSet; import java.util.TreeSet; /** * Checks for changes in a persistent store. Intended to be run both * manually to handle local servlet changes, and periodically to check * for remote connector manager changes. * * @see com.google.enterprise.connector.persist.PersistentStore * @see ChangeListener */ // TODO: Change StoreContext to String and x.getConnectorName() to x. class ChangeDetectorImpl implements ChangeDetector { private final PersistentStore store; private final ChangeListener listener; /** The stamps from the previous run. */ private ImmutableMap<StoreContext, ConnectorStamps> inMemoryInventory = ImmutableMap.of(); /** A sorted set of the keys of {@code inMemoryInventory}. */ private SortedSet<StoreContext> inMemoryInstances = new TreeSet<StoreContext>(); /** * Constructs the detector. * * @param store the persistent store to look for changes in * @param listener the change listener to notify of changes */ ChangeDetectorImpl(PersistentStore store, ChangeListener listener) { this.store = store; this.listener = listener; } @Override public synchronized void detect() { NDC.push("Change"); try { ImmutableMap<StoreContext, ConnectorStamps> persistentInventory = store.getInventory(); SortedSet<StoreContext> persistentInstances = new TreeSet<StoreContext>(persistentInventory.keySet()); // Compare the last known (inMemory) inventory with the new inventory // from the persistent store. Notify ChangeListeners of any differences. // Save in memory, the new inventory of unchanged items and successfully // applied changes. inMemoryInventory = compareInventoriesAndNotifyListeners( inMemoryInstances.iterator(), persistentInstances.iterator(), persistentInventory); inMemoryInstances = persistentInstances; } finally { NDC.pop(); } } /** * Gets the next element of an {@code Iterator} iterator, or * {@code null} if there are no more elements. * * @return the next element or {@code null} */ private StoreContext getNext(Iterator<StoreContext> it) { return it.hasNext() ? it.next() : null; } /** * Iterates over the sorted sets of instance names to find additions * and deletions. When matching names are found, compare the version * stamps for changes in the individual persisted objects. * * @param mi the sorted keys to the in-memory instances * @param pi the sorted keys to the persistent instances * @param persistentInventory the persistent object stamps * @return a new inventory of stamps, derived from the * persistentInventory, but reflecting instantiation failures. */ private ImmutableMap<StoreContext, ConnectorStamps> compareInventoriesAndNotifyListeners( Iterator<StoreContext> mi, Iterator<StoreContext> pi, ImmutableMap<StoreContext, ConnectorStamps> persistentInventory) { // This map will accumulate items for the new in-memory inventory. // Generally, this map will end up being identical to the // persistentInventory. However, failed connector instantiations // may cause changes to be dropped from this map, so that they may // be retried next time around. ImmutableMap.Builder<StoreContext, ConnectorStamps> mapBuilder = new ImmutableMap.Builder<StoreContext, ConnectorStamps>(); StoreContext m = getNext(mi); StoreContext p = getNext(pi); while (m != null && p != null) { // Compare instance names. int diff = m.getConnectorName().compareTo(p.getConnectorName()); NDC.pushAppend((diff < 0 ? m : p).getConnectorName()); try { if (diff == 0) { // Compare the inMemory vs inPStore ConnectorStamps for a // connector instance. Notify ChangeListeners for items whose // Stamps have changed. ConnectorStamps stamps = compareInstancesAndNotifyListeners( m, p, inMemoryInventory.get(m), persistentInventory.get(p)); // Remember the new ConnetorStamps for our new inMemory inventory. mapBuilder.put(p, stamps); // Advance to the next connector instance. m = getNext(mi); p = getNext(pi); } else if (diff < 0) { listener.connectorRemoved(m.getConnectorName()); m = getNext(mi); } else { // diff > 0 try { listener.connectorAdded(p.getConnectorName(), store.getConnectorConfiguration(p)); mapBuilder.put(p, persistentInventory.get(p)); } catch (InstantiatorException e) { // Forget about this one and retry on the next time around. pi.remove(); } p = getNext(pi); } } finally { NDC.pop(); } } while (m != null) { NDC.pushAppend(m.getConnectorName()); try { listener.connectorRemoved(m.getConnectorName()); } finally { NDC.pop(); } m = getNext(mi); } while (p != null) { NDC.pushAppend(p.getConnectorName()); try { listener.connectorAdded(p.getConnectorName(), store.getConnectorConfiguration(p)); mapBuilder.put(p, persistentInventory.get(p)); } catch (InstantiatorException e) { // Forget about this one and retry on the next time around. pi.remove(); } finally { NDC.pop(); } p = getNext(pi); } return mapBuilder.build(); } /** * Compares the version stamps for the given instance. Notify ChangeListeners * of any differences. * * @param m the key for the in-memory instance * @param p the key for the persistent instance * @param ms the stamps for the in-memory instance * @param ps the stamps for the persistent instance * @return possibly modified stamps for the persistent instance */ // TODO: When StoreContext becomes String, we only need one key // parameter because we will have m.equals(p). NOTE: This may be // false now, if the connector type has changed. private ConnectorStamps compareInstancesAndNotifyListeners( StoreContext m, StoreContext p, ConnectorStamps ms, ConnectorStamps ps) { if (compareStamps(ms.getCheckpointStamp(), ps.getCheckpointStamp()) != 0) { listener.connectorCheckpointChanged(p.getConnectorName(), store.getConnectorState(p)); } if (compareStamps(ms.getScheduleStamp(), ps.getScheduleStamp()) != 0) { listener.connectorScheduleChanged(p.getConnectorName(), store.getConnectorSchedule(p)); } // Save configuration for last, because it may fail. if (compareStamps(ms.getConfigurationStamp(), ps.getConfigurationStamp()) != 0) { try { listener.connectorConfigurationChanged(p.getConnectorName(), store.getConnectorConfiguration(p)); } catch (InstantiatorException e) { // Instantiation of the connector failed. Remember a null configuration // stamp so we will try the new configuration again next time through. // This is an attempt to handle connectors that fail instantiation // due to transient causes (such as a server off-line). return new ConnectorStamps(ps.getCheckpointStamp(), null, ps.getScheduleStamp()); } } // Return the original stamps. return ps; } /** * Compares two version stamps. Stamps may be {@code null}, in which * case they are sorted lower than any non-{@code null} object. * * @param memoryStamp the stamp for the in-memory object * @param persistentStamp the stamp for the persistent object * @return a negative integer, zero, or a positive integer as the * in-memory stamp is less than, equal to, or greater than the * persistent stamp * @see java.util.Comparator#compare(Object, Object) */ private int compareStamps(Stamp memoryStamp, Stamp persistentStamp) { if (memoryStamp == null && persistentStamp == null) { return 0; } else if (memoryStamp == null) { return -1; } else if (persistentStamp == null) { return +1; } else { return memoryStamp.compareTo(persistentStamp); } } }