/* Copyright 2004-2014 Jim Voris
*
* 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.qumasoft.qvcslib;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Keyword contracted workfile cache. This is a client-side cache of recent workfiles. The workfiles are stored in memory, in a non-keyword expanded state. The idea
* behind the cache is to capture workfiles so we don't have to do a server round-trip for those cases where we have recently sent the file to the server.
*
* @author Jim Voris
*/
public final class KeywordContractedWorkfileCache {
private static final KeywordContractedWorkfileCache KEYWORD_CONTRACTED_WORKFILE_CACHE = new KeywordContractedWorkfileCache();
private static final int MAXIMUM_CACHE_SIZE = 20_000_000; // 20 Megabytes;
private static final String REVISION_PENDING = "Pending";
/** Store the workfile bytes in a map that is indexed by a String key. */
private Map<KeyByName, byte[]> byNameCache;
private Map<Integer, ByIndexElement> byIndexCache;
private List<KeyByName> byInsertionOrderList;
private int nextIndex = 0;
private int currentCacheSize = 0;
/**
* This is a singleton.
*/
private KeywordContractedWorkfileCache() {
byNameCache = Collections.synchronizedMap(new TreeMap<KeyByName, byte[]>());
byIndexCache = Collections.synchronizedMap(new TreeMap<Integer, ByIndexElement>());
byInsertionOrderList = Collections.synchronizedList(new ArrayList<KeyByName>());
}
/**
* Get the keyword contracted workfile cache singleton.
* @return the keyword contracted workfile cache singleton.
*/
public static KeywordContractedWorkfileCache getInstance() {
return KEYWORD_CONTRACTED_WORKFILE_CACHE;
}
/**
* Add a contracted buffer to the cache.
* @param projectName the project name.
* @param appendedPath the appended path.
* @param shortWorkfileName the short workfile name.
* @param buffer the buffer that we add to the cache.
* @return an index the identifies the buffer within the 'byIndex' cache.
*/
public int addContractedBuffer(String projectName, String appendedPath, String shortWorkfileName, byte[] buffer) {
KeyByName key = new KeyByName(projectName, appendedPath, shortWorkfileName, REVISION_PENDING);
int index = getNextIndex();
getIndexCache().put(Integer.valueOf(index), new ByIndexElement(key, buffer));
return index;
}
/**
* Add a contracted buffer to the cache.
* @param projectName the project name.
* @param appendedPath the appended path.
* @param shortWorkfileName the short workfile name.
* @param revisionString the revision string.
* @param buffer the buffer that we add to the cache.
*/
public void addContractedBuffer(String projectName, String appendedPath, String shortWorkfileName, String revisionString, byte[] buffer) {
KeyByName key = new KeyByName(projectName, appendedPath, shortWorkfileName, revisionString);
addContractedBufferByName(key, buffer);
}
/**
* This method is a little subtle. It is meant to be called by the client code after it receives the server response that updates the logfile information for a file that has
* been checked in. The index value is the 'key' shared between client and server to correlate the checkin request message with the check-in response message. When the client
* gets the response, the response will include the revisionString for the revision that was checked in (the client doesn't 'know' that -- the server figures it out).
* @param index the index used to store the associated buffer. The value for the index is created when the buffer is first saved via the
* {@link #addContractedBuffer(java.lang.String, java.lang.String, java.lang.String, byte[]) } method. It is echoed in the server response, which is how it's value is known
* for calls to this method.
* @param revisionString the revision string to associate with the buffer. On a checkin operation, the client sends the checkin request to the server, along with a buffer
* containing the workfile bytes. But at checkin time, the client does not know the revision string associated with the buffer. When the server sends the response message,
* the server will have determined the revision string, which is the source of the value of the revision string parameter here.
* @return the contracted buffer, or null, if we cannot find it.
*/
public byte[] getContractedBuffer(int index, String revisionString) {
byte[] buffer = null;
Integer indexInteger = Integer.valueOf(index);
ByIndexElement byIndexElement = getIndexCache().get(indexInteger);
if (byIndexElement != null) {
buffer = byIndexElement.getBuffer();
// Remove this entry from the byIndex cache since we don't need it
// there anymore.
getIndexCache().remove(indexInteger);
// Move it to the byName cache (which we limit in size).
KeyByName key = new KeyByName(byIndexElement.getKeyByName(), revisionString);
addContractedBufferByName(key, buffer);
}
return buffer;
}
/**
* Get the contracted workfile buffer by name.
* @param project the project name.
* @param path the appended path.
* @param shortName the short workfile name.
* @param revString the revision string.
* @return the byte[] for the given parameters, or null if it is not found in the cache.
*/
public byte[] getContractedBufferByName(String project, String path, String shortName, String revString) {
KeyByName keyByName = new KeyByName(project, path, shortName, revString);
return getContractedBufferByName(keyByName);
}
private byte[] getContractedBufferByName(KeyByName keyByName) {
byte[] buffer = getByNameCache().get(keyByName);
return buffer;
}
private Map<Integer, ByIndexElement> getIndexCache() {
return byIndexCache;
}
private int getNextIndex() {
return nextIndex++;
}
private Map<KeyByName, byte[]> getByNameCache() {
return byNameCache;
}
private List<KeyByName> getByInsertionOrderList() {
return byInsertionOrderList;
}
private void addContractedBufferByName(KeyByName keyByName, byte[] buffer) {
// Only add it to the cache if it is not already there.
if (null == getContractedBufferByName(keyByName)) {
if (makeRoomForBuffer(buffer.length)) {
getByInsertionOrderList().add(keyByName);
getByNameCache().put(keyByName, buffer);
currentCacheSize += buffer.length;
}
}
}
/**
* Make room for the buffer we just received.
* @param neededBytes the number of bytes that we need for the just received buffer.
* @return true if we were able to make room for the buffer.
*/
private boolean makeRoomForBuffer(int neededBytes) {
boolean madeRoomFlag = false;
if (neededBytes < MAXIMUM_CACHE_SIZE) {
if (getCurrentCacheSize() + neededBytes >= MAXIMUM_CACHE_SIZE) {
// Discard old entries until there is room for the new entry.
while ((getCurrentCacheSize() + neededBytes > MAXIMUM_CACHE_SIZE) && (getByInsertionOrderList().size() > 0)) {
KeyByName keyByName = getByInsertionOrderList().get(0);
byte[] buffer = getContractedBufferByName(keyByName);
if (buffer != null) {
getByNameCache().remove(keyByName);
getByInsertionOrderList().remove(0);
currentCacheSize -= buffer.length;
} else {
throw new QVCSRuntimeException("Keyword contracted cache is broken!!");
}
}
madeRoomFlag = true;
} else {
madeRoomFlag = true;
}
}
return madeRoomFlag;
}
private int getCurrentCacheSize() {
return currentCacheSize;
}
private static class KeyByName implements Comparable {
private final String shortWorkfileName;
private final String projectName;
private final String appendedPath;
private final String byNamekey;
KeyByName(String project, String path, String shortName, String revString) {
projectName = project;
appendedPath = Utility.convertToStandardPath(path);
shortWorkfileName = shortName;
byNamekey = project + appendedPath + shortName + revString;
}
KeyByName(KeyByName oldKey, String revString) {
projectName = oldKey.projectName;
appendedPath = oldKey.appendedPath;
shortWorkfileName = oldKey.shortWorkfileName;
byNamekey = projectName + appendedPath + shortWorkfileName + revString;
}
@Override
public boolean equals(Object o) {
boolean retVal = false;
if (o instanceof KeyByName) {
KeyByName key = (KeyByName) o;
retVal = key.byNamekey.equals(byNamekey);
}
return retVal;
}
@Override
public int hashCode() {
return byNamekey.hashCode();
}
private String getKey() {
return byNamekey;
}
@Override
public int compareTo(Object o) {
if (o instanceof KeyByName) {
KeyByName keyByName = (KeyByName) o;
return getKey().compareTo(keyByName.getKey());
} else {
return -1;
}
}
}
private static class ByIndexElement {
private final KeyByName keyByName;
private final byte[] buffer;
ByIndexElement(KeyByName byNameKey, byte[] buf) {
keyByName = byNameKey;
buffer = buf;
}
byte[] getBuffer() {
return buffer;
}
KeyByName getKeyByName() {
return keyByName;
}
}
}