Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
   package com.fasterxml.clustermate.service.store;
   
   import java.io.*;
   import java.util.ArrayList;
   import java.util.List;
   
   
  
  
Class that handles coordination between front-end service layer (servlet, jax-rs) and back-end storage layer.
  
  public abstract class StoreHandler<
      K extends EntryKey,
      E extends StoredEntry<K>,
      L extends ListItem
  >
      extends HandlerBase
  {
      // Do we want these output? Not for production, at least...
      // TODO: Externalize
      private final static boolean LOG_DUP_PUTS = false;

    
Let's not allow unlimited number of entries to traverse, no matter what.
  
      private final static int MAX_MAX_ENTRIES = 500;

    
And for now strict time limit of 5 seconds
  
      private final static long MAX_LIST_TIME_MSECS = 5000L;
  
      private final static ListLimits DEFAULT_LIST_LIMITS =
              ListLimits.defaultLimits()
                  .withMaxEntries()
                  .withMaxMsecs();
  
      /*
      /**********************************************************************
      /* Helper objects
      /**********************************************************************
       */
  
      protected final ClusterViewByServer _cluster;
      
      protected final Stores<K,E> _stores;
  
      protected final FileManager _fileManager;
  
      protected final TimeMaster _timeMaster;
  
      protected final EntryKeyConverter<K> _keyConverter;
  
      protected final StoredEntryConverter<K,E,L> _entryConverter;
  
      protected final ObjectMapper _objectMapper;
      
      protected final ObjectWriter _listJsonWriter;
      
      protected final ObjectWriter _listSmileWriter;
      
      /*
      /**********************************************************************
      /* Configuration
      /**********************************************************************
       */
  
      protected final ServiceConfig _serviceConfig;
    
    
Whether server-side (auto-)compression is enabled or not.
  
      protected final boolean _cfgCompressionEnabled;
  
      protected final int _cfgDefaultMinTTLSecs;
      protected final int _cfgDefaultMaxTTLSecs;
      
     /*
     /**********************************************************************
     /* Construction
     /**********************************************************************
      */
 
     public StoreHandler(SharedServiceStuff stuffStores<K,E> stores,
             ClusterViewByServer cluster)
     {
          = cluster;
          = stores;
          = stuff.getFileManager();
          = stuff.getTimeMaster();
          = stuff.getKeyConverter();
 
          = stuff.jsonMapper();
          = stuff.jsonWriter();
          = stuff.smileWriter();
 
          = stuff.getServiceConfig();
 
          = stuff.getEntryConverter();
         // seconds used (over millis) to fit in 32-bit int when stored
          = (int) (..getMillis() / 1000L);
     }
 
     /*
     /**********************************************************************
     /* Support for unit tests
     /**********************************************************************
      */
 
     public Stores<K,E> getStores() { return ; }
     
     /*
     /**********************************************************************
     /* Content access (GET)
     /**********************************************************************
      */
 
     public ServiceResponse getEntry(ServiceRequest requestServiceResponse response, K key)
         throws StoreException
     {
         return getEntry(requestresponsekeynull);
     }
     
     public ServiceResponse getEntry(ServiceRequest requestServiceResponse response, K key,
             OperationDiagnostics metadata)
         throws StoreException
     {
         String rangeStr = request.getHeader(.);
         ByteRange range;
         try {
             range = request.findByteRange();
         } catch (IllegalArgumentException e) {
             return invalidRange(responsekeyrangeStre.getMessage());
         }
         String acceptableEnc = request.getHeader(.);
         Storable rawEntry;
 
         try {
             rawEntry = .getEntryStore().findEntry(key.asStorableKey());
         } catch (StoreException e) {
             return _storeError(responsekeye);
         } 
         
         if (metadata != null) {
             metadata.setEntry(rawEntry);
         }
         if (rawEntry == null) {
             return handleGetForMissing(requestresponsekey);
         }
         // second: did we get a tombstone?
         if (rawEntry.isDeleted()) {
             ServiceResponse resp = handleGetForDeleted(requestresponsekeyrawEntry);
             if (resp != null) {
                 return resp;
             }
         }
         // [issue #7]: Conditional GET with Etag
         if (_notChanged(requestrawEntry)) {
             return response.notChanged();
         }
 
         final long accessTime = .currentTimeMillis();
         final E entry = .entryFromStorable(rawEntry);
 
         updateLastAccessedForGet(requestresponseentryaccessTime);
         
         Compression comp = entry.getCompression();
         boolean skipCompression;
 
         // Range to resolve, now that we know full length?
         if (range != null) {
             range = range.resolveWithTotalLength(entry.getActualUncompressedLength());
             final long length = range.calculateLength();
             // any bytes matching? If not, it's a failure
             if (length <= 0L) {
                 return response.badRange(new GetErrorResponse<K>(key"Invalid 'Range' HTTP Header (\""+range+"\")"));
             }
             // note: can not skip decompress if we have to give range...
             skipCompression = false;
         } else {
             skipCompression = (comp != .) && comp.isAcceptable(acceptableEnc);
         }
         
         StreamingResponseContentImpl output;
         
         if (entry.hasExternalData()) { // need to stream from File
             File f = entry.getRaw().getExternalFile();
             output = new StreamingResponseContentImpl(fskipCompression ? null : comprange);
         } else { // inline
             ByteContainer inlined = entry.getRaw().getInlinedData();
             if (!skipCompression) {
                 try {
                     inlined = Compressors.uncompress(inlinedcomp, (intentry.getRaw().getOriginalLength());
                 } catch (IOException e) {
                     return internalGetError(responseekey"Failed to decompress inline data");
                 }
             }
             output = new StreamingResponseContentImpl(inlinedrange);
         }
         // one more thing; add header for range if necessary; also, response code differs
         if (range == null) {
             response = response.ok(output);
         } else {
             response = response.partialContent(outputrange.asResponseHeader());
         }
         // Issue #6: Need to provide Etag, if content hash available
         int contentHash = rawEntry.getContentHash();
         if (contentHash != .) {
             StringBuilder sb = new StringBuilder();
             sb.append('"');
             sb.append(contentHash);
             sb.append('"');
             response = response.addHeader(.sb.toString());
         }
         
         // also need to let client know we left compression in there:
         if (skipCompression) {
             response = response.setBodyCompression(comp.asContentEncoding());
         }
         return response;
     }
 
     protected boolean _notChanged(ServiceRequest requestStorable rawEntry)
     {
         // First: entry must have hash value to compare against
         int contentHash = rawEntry.getContentHash();
         if (contentHash != .) {
             String rangeStr = request.getHeader(.);
             if (rangeStr != null) {
                 rangeStr = rangeStr.trim();
                 if (rangeStr.length() > 2 && rangeStr.charAt(0) == '"') {
                     int ix = rangeStr.lastIndexOf('"');
                     if (ix > 0) {
                         rangeStr = rangeStr.substring(1, ix);
                         try {
                             // Parse as Long to allow for both signed and unsigned representations
                             // of 32-bit hash value
                             long l = Long.parseLong(rangeStr);
                             int i = (intl;
                             return (i == contentHash);
                         } catch (IllegalArgumentException e) { }
                     }
                 }
             }
         }
         return false;
     }
     
     /*
     /**********************************************************************
     /* Content access, metadata
     /**********************************************************************
      */
 
     public ServiceResponse getEntryStats(ServiceRequest requestServiceResponse response, K key)
         throws StoreException
     {
         return getEntryStats(requestresponsekeynull);
     }
     
     // public for calling from unit tests
     public ServiceResponse getEntryStats(ServiceRequest requestServiceResponse response, K key,
             OperationDiagnostics metadata)
         throws StoreException
     {
         // Do we need special handling for Range requests? (GET only?)
     	// Should this update last-accessed as well? (for now, won't)
         Storable rawEntry;
         try {
             rawEntry = .getEntryStore().findEntry(key.asStorableKey());
         } catch (StoreException e) {
             return _storeError(responsekeye);
         } 
         if (metadata != null) {
             metadata.setEntry(rawEntry);
         }
         if (rawEntry == null) {
             return response.notFound(new GetErrorResponse<K>(key"No entry found for key '"+key+"'"));
         }
         // second: did we get a tombstone?
         if (rawEntry.isDeleted()) {
             return response.noContent();
         }
 
         final long accessTime = .currentTimeMillis();
         final E entry = .entryFromStorable(rawEntry);
         updateLastAccessedForHead(requestresponseentryaccessTime);
         
         // Other than this: let's only check out length of data there would be...
         final Compression comp = entry.getCompression();
         long size;
         
         // Would we return content as-is? (not compressed, or compressed using something
         // client accepts)
         String acceptableComp = request.getHeader(.);
         if (comp == . || comp.isAcceptable(acceptableComp)) {
             size = entry.getStorageLength();
         } else {
             size = entry.getActualUncompressedLength();
         }
         return response.ok().setContentLength(size);
     }
 
     /*
     /**********************************************************************
     /* Content insertion (PUT)
     /**********************************************************************
      */
 
     public ServiceResponse putEntry(ServiceRequest requestServiceResponse response,
             K keyInputStream dataIn)
     {
         return putEntry(requestresponsekeydataInnull);
     }
     
     public ServiceResponse putEntry(ServiceRequest requestServiceResponse response,
             K keyInputStream dataInOperationDiagnostics metadata)
     {
         final int checksum = _decodeInt(request.getQueryParameter(.), 0);
         TimeSpan minTTL = findMinTTLParameter(requestkey);
         TimeSpan maxTTL = findMaxTTLParameter(requestkey);
 
         return putEntry(requestresponsekeychecksumdataIn,
                 minTTLmaxTTLmetadata);
     }
 
     
     
     // Public due to unit tests
     public ServiceResponse putEntry(ServiceRequest requestServiceResponse response,
             K keyint checksum,// 32-bit hash by client
             InputStream dataIn,
             TimeSpan minTTLSinceAccessTimeSpan maxTTL,
             OperationDiagnostics stats)
     {
         final long  creationTime = .currentTimeMillis();
 
         // first things first: ensure that request was correctly sent wrt routing
         Compression inputCompression = Compression.forContentEncoding(request.getHeader(
                 .));
         // NOTE: in future, may want to allow client to specify "do not compress"; if so,
         // we would pass Compression.NONE explicitly: null means "try to use whatever"
         if (inputCompression == .) {
             inputCompression = null;
         }
         LastAccessUpdateMethod lastAcc = _findLastAccessUpdateMethod(requestkey);
 
         // assumption here is that we may be passed hash code of orig content, but
         // not that of compressed (latter is easy to calculate on server anyway)
         StorableCreationMetadata stdMetadata = new StorableCreationMetadata(inputCompression,
         		checksum, 0);
 
         int minTTLSecs = (minTTLSinceAccess == null) ? findMinTTLDefaultSecs(requestkey)
                 : (int) (minTTLSinceAccess.getMillis() / 1000);
         int maxTTLSecs = (maxTTL == null) ? findMaxTTLDefaultSecs(requestkey)
                 : (int) (maxTTL.getMillis() / 1000);
 
         ByteContainer customMetadata = .createMetadata(creationTime,
                 ((lastAcc == null) ? 0 : lastAcc.asByte()),
                 minTTLSecsmaxTTLSecs);
         StorableCreationResult result;
         try {
             /* This gets quite convoluted but that's how it goes: if undelete is
              * allowed, we must use different method:
              */
             if (.) {
                 result = .getEntryStore().upsertConditionally(key.asStorableKey(),
                         dataInstdMetadatacustomMetadatatrue,
                         .);
             } else {
                 result = .getEntryStore().insert(key.asStorableKey(),
                         dataInstdMetadatacustomMetadata);
             }
         } catch (StoreException.Input e) { // something client did wrong
             switch (e.getProblem()) {
             case :
                 return response.badRequest
                         (PutResponse.badArg(key"Bad Compression information passed: "+e.getMessage()));
             case :
                 return response.badRequest
                         (PutResponse.badArg(key"Bad Checksum information passed: "+e.getMessage()));
             case :
                 return response.badRequest
                         (PutResponse.badArg(key"Bad Length information passed: "+e.getMessage()));
             }
             return internalPutError(responsekey,
                     e"Failed to PUT an entry: "+e.getMessage());
         } catch (IOException e) {
             return internalPutError(responsekey,
             		e"Failed to PUT an entry: "+e.getMessage());
         }
         // And then check whether it was a dup put; and if so, that checksums match
         Storable prev = result.getPreviousEntry();
         if (prev != null) {
             if (stats != null) {
                 stats.setEntry(result.getNewEntry());
             }
             _logDuplicatePut(key);
             // first: will not allow "recreating" a soft-deleted entry
             if (prev.isDeleted()) {
                 if (!.) {
                     String prob = "Failed PUT: trying to recreate deleted entry '"+key+"'";
                     return response.gone(PutResponse.error(keyprevprob));
                 }
                 // otherwise... we are ok, iff checksums match
             }
             // second: verify that checksums match:
             String prob = _verifyChecksums(prevstdMetadata);
             if (prob != null) {
                 return response.conflict(PutResponse.error(keyprev"Failed PUT: trying to "
                         +(prev.isDeleted() ? "undelete" : "overwrite")
                         +" entry '"+key+"' but "+prob));
             }
         } else if (stats != null) {
             stats.setEntry(result.getNewEntry());
         }
         return response.ok(PutResponse.ok(keyresult.getNewEntry()));
     }
     
     private String _verifyChecksums(Storable oldEntryStorableCreationMetadata newEntry)
     {
         if (oldEntry.getContentHash() != newEntry.contentHash) { 
             return "checksums differ; old had 0x"+Integer.toHexString(oldEntry.getContentHash())+", new 0x"+Integer.toHexString(newEntry.contentHash);
         }
         if (oldEntry.getCompressedHash() != newEntry.compressedContentHash) {
             return "checksumForCompressed differ; old had 0x"+Integer.toHexString(oldEntry.getCompressedHash())
                     +", new 0x"+Integer.toHexString(newEntry.compressedContentHash);
         }
         Compression oldC = oldEntry.getCompression();
         Compression newC = newEntry.compression;
         if (newC == null) {
             newC = .;
         }
         if (oldC != newC) {
             return "entity compression differs; old had "+oldC+" new "+newC;
         }
         return null;
     }
     
     /*
     /**********************************************************************
     /* Content deletion
     /**********************************************************************
      */
 
     public ServiceResponse removeEntry(ServiceRequest requestServiceResponse response, K key)
         throws IOExceptionStoreException
     {
         return removeEntry(requestresponsekeynull);
     }
     
     public ServiceResponse removeEntry(ServiceRequest requestServiceResponse response, K key,
             OperationDiagnostics metadata)
         throws IOExceptionStoreException
     {
         StorableDeletionResult result;
         try {
             result = .getEntryStore().softDelete(key.asStorableKey(), truetrue);
         } catch (StoreException e) {
             return _storeError(responsekeye);
         } 
             
         /* Even without match, we can claim it is ok... should we?
          * From idempotency perspective, result is that there is no such
          * entry; so let's allow that and just give the usual 204.
          */
         long creationTime = 0L;
         
         // also: if deletion succeeded, may need to delete actual physical file:
         if (result != null && result.hadEntry()) {
             Storable rawEntry = result.getEntry();
             if (metadata != null) {
                 metadata.setEntry(rawEntry);
             }
             E entry = .entryFromStorable(keyrawEntry);
             creationTime = entry.getCreationTime();
         }
         return response.ok(new DeleteResponse<K>(keycreationTime));
     }
 
     /*
     /**********************************************************************
     /* Listing entries
     /**********************************************************************
      */
    
    
End point clients use to list entries with given name prefix,

Parameters:
request
response
prefix Path prefix to use for filtering out entries not to list
metadata Diagnostic information to update, if any
Returns:
Modified response object
 
     @SuppressWarnings("unchecked")
     public <OUT extends ServiceResponse> OUT listEntries(ServiceRequest request, OUT response,
             final K prefixOperationDiagnostics metadata)
         throws StoreException
     {
         // simple validation first
         if (prefix == null) {
             return (OUT) badRequest(response"Missing path parameter for 'listEntries'");
         }
         String typeStr = request.getQueryParameter(.);
         ListItemType listType = ListItemType.find(typeStr);
         if (listType == null) {
             if (typeStr == null || typeStr.isEmpty()) {
                 return (OUT) badRequest(response"Missing query parameter '"
                         +.+"'");
             }
             return (OUT) badRequest(response"Invalid query parameter '"
                     +.+"', value '"+typeStr+"'");
         }
         
         /* First a sanity check: prefix should map to our active or passive range.
          * If not, we should not have any data to list; so let's (for now?) fail request:
          */
         int rawHash = .routingHashFor(prefix);
         // note: _cluster is null for testing, not for regular operation
         if (( != null) && !.getLocalState().inAnyRange(rawHash)) {
             return (OUT) badRequest(response"Invalid prefix: not in key range (%s) of node",
                     .getLocalState().totalRange());
         }
 
         // Then extract 'lastSeen', if it's passed:
         StorableKey lastSeen = null;
         if (b64str != null && b64str.length() > 0) {
             try {
                 // Jackson can do base64 decoding, and this is the easiest way
                 byte[] lastSeenRaw = .convertValue(b64strbyte[].class);
                 lastSeen = new StorableKey(lastSeenRaw);
             } catch (Exception e) {
                 return (OUT) badRequest(response"Invalid '"+.
                         +"' value; not valid base64 encoded byte sequence");
             }
         }
         // Otherwise can start listing
         boolean useSmile = _acceptSmileContentType(request);
         final StorableKey rawPrefix = prefix.asStorableKey();
         ListLimits limits = ;
 
         if (maxStr != null) {
             limits = limits.withMaxEntries(_decodeInt(maxStrlimits.getMaxEntries()));
         }
 
         // !!! TODO: allow listing of tombstones?
         
         ListResponse<?> listResponse = null;
         
         switch (listType) {
         case :
             {
                 List<StorableKeyids = _listIds(rawPrefixlastSeenlimits);
                 listResponse = new ListResponse.IdListResponse(ids_last(ids));
             }
             break;
         case :
             {
                 List<StorableKeyids = _listIds(rawPrefixlastSeenlimits);
                 ArrayList<Stringnames = new ArrayList<String>(ids.size());
                 for (StorableKey id : ids) {
                     names.add(.rawToString(id));
                 }
                 listResponse = new ListResponse.NameListResponse(names_last(ids));
             }
             break;
         case :
         case :
             {
                 List<ListItemitems = _listItems(listTyperawPrefixlastSeenlimits);
                 ListItem lastItem = _last(items);
                 if (listType == .) {
                     listResponse = new ListResponse.MinimalItemListResponse(items,
                             (lastItem == null) ? null : lastItem.getKey());
                 } else {
                     listResponse = new ListResponse<ListItem>(items,
                             (lastItem == null) ? null : lastItem.getKey());
                 }
             }
             break;
         default:
             throw new IllegalStateException();
         }
         
         if (metadata != null) {
             metadata.setItemCount(listResponse.size());
         }
         
         final ObjectWriter w = useSmile ?  : ;
         final String contentType = useSmile ? ..toString()
                 : ..toString();
         return (OUT) response.ok(contentTypenew StreamingEntityImpl(wlistResponse));
     }
 
     protected final static <T> T _last(List<T> list) {
         if (list == null || list.isEmpty()) {
             return null;
         }
         return list.get(list.size() - 1);
     }
     
     protected List<ListItem_listItems(ListItemType itemTypeStorableKey prefix,
             StorableKey lastSeenListLimits limits)
         throws StoreException
     {
         // we could check if all entries were iterated (with result code); for now we won't
         ListItemsCallback cb = (itemType == .)
                 ? new FullListItemsCallback(prefixlimits)
                 : new ListItemsCallback(prefixlimits)
                 ;
         /* Two cases; either starting without last seen -- in which case we should include
          * the first entry -- or with last-seen, in which case it is to be skipped.
          */
         if (lastSeen == null) {
             // we could check if all entries were iterated (with result code); for now we won't
             /*IterationResult r =*/ .getEntryStore().iterateEntriesByKey(cbprefix);
         } else {
             // we could check if all entries were iterated (with result code); for now we won't
             /*IterationResult r =*/ .getEntryStore().iterateEntriesAfterKey(cblastSeen);
         }
         return cb.getResult();
     }
 
     protected List<StorableKey_listIds(final StorableKey prefixStorableKey lastSeen,
             final ListLimits limits)
         throws StoreException
     {
         final long maxTime = .currentTimeMillis() + limits.getMaxMsecs();
         final int maxEntries = limits.getMaxEntries();
         final ArrayList<StorableKeyresult = new ArrayList<StorableKey>(100);
         final boolean includeTombstones = limits.getIncludeTombstones();
 
         StorableIterationCallback cb = new StorableIterationCallback() {
             public int counter// to avoid checking systime too often
 
             @Override
             public IterationAction verifyKey(StorableKey key) {
                 if (!key.hasPrefix(prefix)) {
                     return .;
                 }
                 // Can add right away, if it's ok to include tombstones
                 if (includeTombstones) {
                     return _addEntry(key);
                 }
                 // otherwise need to peek in entry itself
                 return .;
             }
 
             private final IterationAction _addEntry(StorableKey key)
             {
                 result.add(key);
                 if (result.size() >= maxEntries) {
                     return .;
                 }
                 if (((++ & 15) == 0) // check for every 16 items
                     && .currentTimeMillis() >= maxTime) {
                         return .;
                 }
                 // no need for entry; key has all the data
                 return .;
             }
 
             @Override
             public IterationAction processEntry(Storable entrythrows StoreException {
                 if (includeTombstones || entry.isDeleted()) {
                     return .;
                 }
                 return _addEntry(entry.getKey());
             }
         };
         /* Two cases; either starting without last seen -- in which case we should include
          * the first entry -- or with last-seen, in which case it is to be skipped.
          */
         if (lastSeen == null) {
             // we could check if all entries were iterated (with result code); for now we won't
             /*IterationResult r =*/ .getEntryStore().iterateEntriesByKey(cbprefix);
         } else {
             // we could check if all entries were iterated (with result code); for now we won't
             /*IterationResult r =*/ .getEntryStore().iterateEntriesAfterKey(cblastSeen);
         }
         return result;
     }
 
     /*
     /**********************************************************************
     /* Customizable handling for query parameter access, defaulting
     /**********************************************************************
      */

    
Overridable helper method used for figuring out request parameter used to pass "minimum time-to-live since last access" (or, if no access tracked, since creation).
 
     protected TimeSpan findMinTTLParameter(ServiceRequest request, K key)
     {
         String paramValue = request.getQueryParameter(paramKey);
         return _isEmpty(paramValue) ? null : new TimeSpan(paramValue);
     }

    
Overridable helper method used for figuring out request parameter used to pass "maximum time-to-live since creation".
 
     protected TimeSpan findMaxTTLParameter(ServiceRequest request, K key)
     {
         String paramKey = .;
         String paramValue = request.getQueryParameter(paramKey);
         return _isEmpty(paramValue) ? null : new TimeSpan(paramValue);
     }
 
     protected int findMinTTLDefaultSecs(ServiceRequest request, K key) {
         return ;
     }
 
     protected int findMaxTTLDefaultSecs(ServiceRequest request, K key) {
         return ;
     }
     
     /*
     /**********************************************************************
     /* Customizable handling for deleted and missing entries
     /**********************************************************************
      */
    
    
Method called to determine what to do when no entry was found for GET with specified key. Method must return a non-null response to return to sender.
 
     protected ServiceResponse handleGetForDeleted(ServiceRequest requestServiceResponse response,
             K keyStorable contents)
     {
         if (.) {
             return response.noContent();
         }
         return response.notFound(new GetErrorResponse<K>(key"No entry found for key '"+key+"'"));
     }

    
Method called to determine what to do when a (soft-)deleted entry is found with GET. Choices include sending a specific response (404 or 204, for example; or returning null to indicate "handle normally".
 
     protected ServiceResponse handleGetForMissing(ServiceRequest requestServiceResponse response,
             K key)
     {
         return response.notFound(new GetErrorResponse<K>(key"No entry found for key '"+key+"'"));
     }
 
     /*
     /**********************************************************************
     /* Abstract methods for sub-classes
     /**********************************************************************
      */

    
Method called for GET operations, to figure out which method of updating "last-access" information should be used, if any.
 
     protected abstract LastAccessUpdateMethod _findLastAccessUpdateMethod(ServiceRequest request, K key);
    
    
Method called to let implementation update last-accessed timestamp if necessary when a piece of content is succesfully fetched with GET (exists and either is not soft-deleted, or passes check for deletion)
 
     protected abstract void updateLastAccessedForGet(ServiceRequest requestServiceResponse response,
             E entrylong accessTime);
 
     protected abstract void updateLastAccessedForHead(ServiceRequest requestServiceResponse response,
             E entrylong accessTime);
     
     /*
     /**********************************************************************
     /* Error reporting
     /**********************************************************************
      */
 
     @SuppressWarnings("unchecked")
     @Override
     protected <OUT extends ServiceResponse> OUT _badRequest(ServiceResponse responseString msg) {
         return (OUT) response.badRequest(msg).setContentTypeText();
     }
 
     @SuppressWarnings("unchecked")
     protected  <OUT extends ServiceResponse> OUT  _storeError(ServiceResponse response, K key,
             StoreException e) {
         String msg;
         if (key == null) {
             msg = "StoreException (key "+key+"): "+e.getMessage();
         } else {
             msg = "StoreException: "+e.getMessage();
         }
         
         // 18-Mar-2013, tatu: StoreExceptions are special enough (unless proven otherwise)
         //  such that we do want to log details -- to be tuned as necessary
         .error(msge);
         
         return (OUT) response.serviceTimeout(msg).setContentTypeText();
     }
 
     private ServiceResponse invalidRange(ServiceResponse response,
             K keyString valueString errorMsg)
     {
         return response.badRequest
                 (PutResponse.badArg(key"Invalid 'Range' HTTP Header (\""+value+"\"), problem: "+errorMsg));
     }
 
     private ServiceResponse internalGetError(ServiceResponse response,
             Exception e, K keyString msg)
     {
         msg = "Failed GET, key '"+key+"': "+msg;
         if (e != null) {
             msg += " (error message: "+e.getMessage()+")";
             .error("Internal error for GET request: "+msge);
         }
         return response.internalError(new GetErrorResponse<K>(keymsg));
     }
 
     private ServiceResponse internalPutError(ServiceResponse response,
             K keyThrowable eString msg)
     {
         if (e != null) {
             e = _peel(e);
             msg = msg + ": "+e.getMessage();
             .error("Internal error for PUT request: "+msge);
         }
         return response.internalError(PutResponse.error(keymsg));
     }
 
     /*
     /**********************************************************************
     /* Internal methods, diagnostics
     /**********************************************************************
      */
 
     protected void _logDuplicatePut(K key)
     {
         if () {
             .info("Duplicate PUT for key '{}'; success, same checksum"key);
         }
     }
     
     /*
     /**********************************************************************
     /* Other helper methods
     /**********************************************************************
      */
 
     private boolean _isEmpty(String value)
     {
         return (value == null || value.length() == 0);
     }
    
    
Crappy little parse function that will try to avoid exception if possible (exceptions are exceptionally costly to construct), defer to JDK standard parsing if thing looks ok
 
     private final int _decodeInt(String inputint defaultValue)
     {
         if (input == null || input.length() == 0) {
             return defaultValue;
         }
         if ("0".equals(input)) {
             return 0;
         }
         final int len = input.length();
         int i = 0;
         if (input.charAt(0) == '-') {
             if (len > 1) {
                 ++i;
             }
         }
         for (; i < len; ++i) {
             char c = input.charAt(i);
             if (c > '9' || c < '0') {
                 // invalid... error or default?
                 return defaultValue;
             }
         }
         // let's allow both positive (unsigned 32 int) and negative (signed); to do that
         // need to parse as Long, cast down.
         try {
             return (int) Long.parseLong(input);
         } catch (IllegalArgumentException e) {
             return defaultValue;
         }
     }
     
     protected static Throwable _peel(Throwable t) {
         while (t.getCause() != null) {
                 t = t.getCause();
         }
         return t;
     }
 
     /*
     /**********************************************************************
     /* Helper classes
     /**********************************************************************
      */
 
     protected static class FullListItemsCallback extends ListItemsCallback
     {
         public FullListItemsCallback(TimeMaster timeMasterStoredEntryConverter<?,?,?> entryConverter,
                 StorableKey prefixListLimits limits) {
             super(timeMasterentryConverterprefixlimits);
         }
 
         @Override
         protected ListItem toListItem(Storable entry) {
             return .fullListItemFromStorable(entry);
         }
     }
     
     protected static class ListItemsCallback extends StorableIterationCallback
     {
         protected final TimeMaster _timeMaster;
         protected final StoredEntryConverter<?,?,?> _entryConverter;
 
         protected final StorableKey prefix;
         protected final long maxTime;
         protected final int maxEntries;
         protected final boolean includeTombstones;
 
         protected final ArrayList<ListItemresult = new ArrayList<ListItem>(100);
         
         public int counter// to avoid checking systime too often
         
         public ListItemsCallback(TimeMaster timeMasterStoredEntryConverter<?,?,?> entryConverter,
                 StorableKey prefixListLimits limits)
         {
              = timeMaster;
              = entryConverter;
              = .currentTimeMillis() + limits.getMaxMsecs();
              = limits.getMaxEntries();
              = limits.getIncludeTombstones();
             this. = prefix;
         }
 
         public List<ListItemgetResult() { return ; }
         
         @Override
         public IterationAction verifyKey(StorableKey key) {
             if (!key.hasPrefix()) {
                 return .;
             }
             if ((++ & 15) == 0) { // check for every 16 items
                 if (!.isEmpty()) { // do NOT terminate after at least one entry included
                     if (.currentTimeMillis() >= ) {
                         return .;
                     }
                 }
             }
             return .;
         }
 
         @Override
         public IterationAction processEntry(Storable entrythrows StoreException
         {
             if ( || !entry.isDeleted()) {
                 .add(toListItem(entry));
                 if (.size() >= ) {
                     return .;
                 }
             }
             return .;
         }
        protected ListItem toListItem(Storable entry) {
            return .minimalListItemFromStorable(entry);
        }
    }