@Component
public class RepositoryManagerComponent {

    private static final Logger logger__ =
        getLogger(RepositoryManagerComponent.class);

    private static final String REPO_BASE_DEFAULT = "WEB-INF/work";

    private final RepositoryManager repositoryManager_;

    @Injectable
    public RepositoryManagerComponent(final ServletContext context) {
        repositoryManager_ = createInitialAdminRepository(context);
    }

    public RepositoryManager getRepositoryManager() {
        return repositoryManager_;
    }

    private static final RepositoryManager getRepositoryManager(final ServletContext context) {
        String repositoryBase = getRepositoryBase();
        if(repositoryBase == null) {
            logger__.warn("Config property '" + HAVALO_REPO_BASE_CONFIG_PROPERTY +
                "' was not set, using default repository base: " +
                REPO_BASE_DEFAULT);
            repositoryBase = REPO_BASE_DEFAULT;
        }
        // If the provided repository base path starts with a slash, then
        // interpret the location as an absolute path on disk.  Otherwise,
        // no preceding slash indicates a path relative to the web application
        // root directory.
        final File realPath;
        if(repositoryBase.startsWith("/")) {
            realPath = new File(repositoryBase);
        } else {
            realPath = new File(context.getRealPath("/" + repositoryBase));
        }
        logger__.info("Using repository root at: " + realPath.getAbsolutePath());
        final int maxFilenameLength = getMaxFilenameLength();
        logger__.info("Max repository object filename length: " +
            maxFilenameLength);
        return new RepositoryManager(realPath, maxFilenameLength);
    }

    private static final RepositoryManager createInitialAdminRepository(final ServletContext context) {
        RepositoryManager repoManager = null;
        try {
            repoManager = getRepositoryManager(context);
            final String adminUUID = getHavaloAdminUUID();
            if(adminUUID == null) {
                final String msg = "Config property '" +
                    HAVALO_ADMIN_API_UUID_PROPERTY + "' not set. Cannot " +
                    "start until this property contains a valid UUID.";
                logger__.error(msg);
                throw new BootstrapException(msg);
            } else {
                try {
                    UUID.fromString(adminUUID);
                } catch (Exception e) {
                    logger__.error("Config property '" +
                        HAVALO_ADMIN_API_UUID_PROPERTY + "' was set, but " +
                        "did not contain a valid UUID. Cannot " +
                        "start until this property contains a well " +
                        "formed UUID.", e);
                    throw new BootstrapException(e);
                }
            }
            // Verify a proper admin API accout secret is set.
            final String adminSecret = getHavaloAdminSecret();
            if(adminSecret == null) {
                final String msg = "Config property '" +
                    HAVALO_ADMIN_API_SECRET_PROPERTY + "' not set. Cannot " +
                    "start until this property contains a valid secret.";
                logger__.error(msg);
                throw new BootstrapException(msg);
            }
            logger__.debug("Admin API account initialized (uuid=" + adminUUID +
                ", secret=" + abbreviate(adminSecret, 8) + ")");
            // Create a new keypair for the default ADMIN level user.
            final KeyPair adminKeyPair = new KeyPair(new HavaloUUID(adminUUID),
                adminSecret, Arrays.asList(new UserRole[]{ADMIN}));
            // Actually attempt to create a new Repository for the Admin user.
            // This should work, if not, bail the whole app.
            repoManager.createRepository(adminKeyPair.getKey(), adminKeyPair);
            return repoManager;
        } catch (RepositoryCreationException e) {
            // Log in TRACE and continue silently.  This is a normal case,
            // when the admin repo has already been created on firstboot
            // but Havalo is being re-started.
            logger__.trace("Failed to create ADMIN user repository -- " +
                "repository already exists.", e);
        } catch (Exception e) {
            // Hm, something else went wrong on startup, need to log
            // and then bail.  The application cannot continue at this point.
            logger__.error("Failed to create ADMIN user repository -- " +
                "cannot continue, giving up.", e);
            throw new BootstrapException(e);
        }
        return repoManager;
    }

} public abstract class HavaloApiController {

    protected final RepositoryManager repositoryManager_;

    public HavaloApiController(final RepositoryManager repositoryManager) {
        repositoryManager_ = checkNotNull(repositoryManager,
            "Repository manager cannot be null.");
    }

    protected final Repository createRepository(final HavaloUUID id,
                                                final KeyPair keyPair) {
        return repositoryManager_.createRepository(id, keyPair);
    }

    protected final void deleteRepository(final HavaloUUID id) {
        repositoryManager_.deleteRepository(id);
    }

    protected final Repository getRepository(final HavaloUUID userId) {
        return repositoryManager_.getRepository(userId);
    }

    protected final HashedFileObject getHashedFileObject(final Repository repo,
                                                         final String key,
                                                         final boolean failIfNotFound) {
        return repositoryManager_.getHashedFileObject(repo, key, failIfNotFound);
    }

    protected final HashedFileObject getHashedFileObject(final Repository repo,
                                                         final ObjectKey key,
                                                         final boolean failIfNotFound) {
        return getHashedFileObject(repo, key.getDecodedKey(), failIfNotFound);
    }

    protected final HashedFileObject getHashedFileObject(final Repository repo,
                                                         final String key) {
        return getHashedFileObject(repo, key, false);
    }

    protected final HashedFileObject getHashedFileObject(final Repository repo,
                                                         final ObjectKey key) {
        return getHashedFileObject(repo, key.getDecodedKey());
    }

    protected final DiskObject getCanonicalObject(final Repository repo,
                                                  final HashedFileObject hfo,
                                                  final boolean makeParentDirs) {
        return repositoryManager_.getCanonicalObject(repo, hfo, makeParentDirs);
    }

    protected final DiskObject getCanonicalObject(final Repository repo,
                                                  final HashedFileObject hfo) {
        return getCanonicalObject(repo, hfo, false);
    }

    protected final HashedFileObject deleteHashedFileObject(final Repository repo,
                                                            final String key,
                                                            final String ifMatch) {
        return repositoryManager_.deleteHashedFileObject(repo, key, ifMatch);
    }

    protected final HashedFileObject deleteHashedFileObject(final Repository repo,
                                                            final ObjectKey key,
                                                            final String ifMatch) {
        return repositoryManager_.deleteHashedFileObject(repo,
            key.getDecodedKey(), ifMatch);
    }

    protected final HashedFileObject deleteHashedFileObject(final HavaloUUID id,
                                                            final String key,
                                                            final String ifMatch) {
        return repositoryManager_.deleteHashedFileObject(id, key, ifMatch);
    }

    protected final HashedFileObject deleteHashedFileObject(final HavaloUUID id,
                                                            final ObjectKey key,
                                                            final String ifMatch) {
        return deleteHashedFileObject(id, key.getDecodedKey(), ifMatch);
    }

    protected final void flushRepository(final Repository repo) {
        repositoryManager_.flushRepository(repo);
    }

} public final class DiskObject extends HavaloFileEntity implements Serializable {

	private static final long serialVersionUID = -3291763185439424118L;
	
	// Not serialized by GSON
	private transient final File file_;
	
	// Not serialized by GSON
	private transient final File tempFile_;
	
	public DiskObject(String name, File file, File tempFile) {
		super(name);
		file_ = file;
		tempFile_ = tempFile;
	}
	
	public DiskObject(String name, File file) {
		this(name, file, null);
	}
	
	public DiskObject(String name) {
		this(name, null);
	}
	
	// For GSON
	public DiskObject() {
		this(null);
	}
	
	public File getFile() {
		return file_;
	}
	
	public File getTempFile() {
		return tempFile_;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name_ == null) ? 0 : name_.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		DiskObject other = (DiskObject) obj;
		if (name_ == null) {
			if (other.name_ != null)
				return false;
		} else if (!name_.equals(other.name_))
			return false;
		return true;
	}
	
} public final class HashedFileObject extends HavaloFileEntity implements Serializable {
	
	private static final long serialVersionUID = 7496664023986725650L;
						
	@SerializedName("headers")
	private final Map<String,List<String>> headers_;
		
	public HashedFileObject(final String name, final Map<String,List<String>> headers) {
		super(name);
		headers_ = headers;
	}
	
	public HashedFileObject(final String name) {
		this(name, new ConcurrentHashMap<String,List<String>>());
	}
		
	// For GSON
	public HashedFileObject() {
		this(null);
	}
	
	public HashedFileObject setLastModified(Date lastModified) {		
		return setLastModified(getNewInstance().format(lastModified));
	}
	
	public HashedFileObject setLastModified(long lastModified) {
		return setLastModified(getNewInstance().format(new Date(lastModified)));
	}
	
	public HashedFileObject setLastModified(final String lastModified) {
		return setHeader(LAST_MODIFIED, lastModified);
	}
	
	public HashedFileObject setETag(final String eTag, final boolean quote) {
		return setHeader(ETAG, (quote) ? String.format("\"%s\"", eTag) : eTag);
	}
	
	public HashedFileObject setETag(String eTag) {
		return setETag(eTag, true);
	}
	
	/**
	 * Set the Content-Length of this entity.
	 * @param contentLength
	 * @return
	 */
	public HashedFileObject setContentLength(final long contentLength) {
		return setHeader(CONTENT_LENGTH, Long.toString(contentLength));
	}
	
	/**
	 * Set the Content-Type of this entity.
	 */
	public HashedFileObject setContentType(final String contentType) {
		return setHeader(CONTENT_TYPE, contentType);
	}
	
	/**
	 * Set the given, single header value under the given name.  If a header
	 * already exists with the given name, the old value is overwritten.
	 * @param headerName
	 * @param headerValue
	 * @return
	 */
	public HashedFileObject addHeader(final String headerName,
		final String headerValue) {
		List<String> list = null;
		if((list = headers_.get(headerName)) == null) {
			list = new ArrayList<String>();
		}
		list.add(headerValue);
		headers_.put(headerName, list);
		return this;
	}
	
	public HashedFileObject setHeader(final String headerName,
		final String headerValue) {
		final List<String> list = new ArrayList<String>();
		list.add(headerValue);
		headers_.put(headerName, list);
		return this;
	}
	
	public List<String> getHeader(String name) {
		return headers_.get(name);
	}
	
	public String getFirstHeader(String name) {
		final List<String> headers;
		if((headers = getHeader(name)) != null) {
			return headers.get(0);
		}
		return null;
	}
	
	/**
	 * Returns a thread-safe (deep) copy of the underlying {@link HttpHeaders}
	 * object associated with this {@link HashedFileObject} entity.  Any
	 * modifications made to the returned {@link HttpHeaders} will not impact
	 * the {@link HttpHeaders} stored with this entity.
	 * @return
	 */
	public Map<String,List<String>> getHeaders() {
		return new ConcurrentHashMap<String,List<String>>(headers_);
	}
	
	// Straight from Eclipse
	// Only compares the name
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name_ == null) ? 0 : name_.hashCode());
		return result;
	}

	// Straight from Eclipse
	// Only compares the name
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		HashedFileObject other = (HashedFileObject) obj;
		if (name_ == null) {
			if (other.name_ != null)
				return false;
		} else if (!name_.equals(other.name_))
			return false;
		return true;
	}
	
} public final class KeyPair extends HavaloEntity implements Serializable {
	
	private static final long serialVersionUID = -844726755873142920L;

	private static final KolichSecureRandom random__ = new KolichSecureRandom();
	
	@SerializedName("key")
	private HavaloUUID idKey_;
	
	@SerializedName("secret")
	private String secret_;
		
	@SerializedName("roles")
	private List<UserRole> roles_;
	
	public KeyPair(HavaloUUID idKey, String secret, List<UserRole> roles) {
		idKey_ = idKey;
		secret_ = secret;
		roles_ = roles;
	}
	
	public KeyPair(HavaloUUID idKey, List<UserRole> roles) {
		this(idKey, generateRandomSecret(), roles);
	}
	
	public KeyPair(List<UserRole> roles) {
		this(new HavaloUUID(), roles);
	}
	
	// For GSON
	public KeyPair() {
		this(Arrays.asList(new UserRole[]{UserRole.USER}));
	}
		
	public HavaloUUID getKey() {
		return idKey_;
	}
	
	public KeyPair setKey(HavaloUUID id) {
		idKey_ = id;
		return this;
	}

	public String getSecret() {
		return secret_;
	}

	public KeyPair setSecret(String secret) {
		secret_ = secret;
		return this;
	}
		
	public List<UserRole> getRoles() {
		return new ArrayList<UserRole>(roles_);
	}
		
	public KeyPair setRoles(List<UserRole> roles) {
		roles_ = roles;
		return this;
	}
	
	public boolean isAdmin() {
		checkNotNull(roles_, "Checking for admin status, role " +
			"list cannot be null.");
		return roles_.contains(ADMIN);
	}
	
	private static final String generateRandomSecret() {
		return newStringUtf8(encodeBase64URLSafe(random__.getRandom()));
	}

	// Straight from Eclipse
	// Only compares the ID and secret fields.
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((idKey_ == null) ? 0 : idKey_.hashCode());
		result = prime * result + ((secret_ == null) ? 0 : secret_.hashCode());
		return result;
	}

	// Straight from Eclipse
	// Only compares the ID and secret fields.
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		KeyPair other = (KeyPair) obj;
		if (idKey_ == null) {
			if (other.idKey_ != null)
				return false;
		} else if (!idKey_.equals(other.idKey_))
			return false;
		if (secret_ == null) {
			if (other.secret_ != null)
				return false;
		} else if (!secret_.equals(other.secret_))
			return false;
		return true;
	}
	
} public final class Repository extends StoreableEntity implements Serializable {
	
	private static final long serialVersionUID = 8231197578192198012L;
	
	@SerializedName("repo_dir")
	private final File repositoryDir_;
	
	@SerializedName("id")
	private HavaloUUID repoId_;
		
	@SerializedName("key_pair")
	private KeyPair keyPair_;
	
	@SerializedName("objects")
	private final Trie<String, HashedFileObject> objects_;
	
	public Repository(File repositoryDir, HavaloUUID repoId) {
		super((repoId != null) ? repoId.toString() : null);
		repositoryDir_ = repositoryDir;
		repoId_ = repoId;
		objects_ = new PatriciaTrie<>();
	}
	
	// For GSON
	public Repository() {
		this(null, null);
	}
	
	@Override
	public String getKey() {
		checkNotNull(repoId_);
		return repoId_.toString();
	}
	
	public File getFile() {
		return repositoryDir_;
	}
	
	public HavaloUUID getRepoId() {
		return repoId_;
	}
	
	public Repository setRepoId(HavaloUUID repoId) {
		repoId_ = repoId;
		return this;
	}
	
	public KeyPair getKeyPair() {
		return keyPair_;
	}
	
	public Repository setKeyPair(KeyPair keyPair) {
		keyPair_ = keyPair;
		return this;
	}
	
	public Repository addObject(String key, HashedFileObject object) {
		// Explicitly synchronized around the objects Trie such that no
		// internal conflicts occur during GSON serialization (when GSON
		// is serializing this object to JSON).  NOTE: The TrieTypeAdapter
		// is also synchronized on the Trie to be serialized (such that no
		// modifications are allowed to the Trie while it's being serialized).
		// We are _not_ synchronizing around individual operations on the Trie
		// (e.g., a single get, put, remove, etc.) but rather the entire Trie
		// on each operation.
		synchronized(objects_) {
			objects_.put(key, object);
		}
		return this;
	}
	
	public HashedFileObject deleteObject(String key) {
		// Explicitly synchronized around the objects Trie such that no
		// internal conflicts occur during GSON serialization (when GSON
		// is serializing this object to JSON).  NOTE: The TrieTypeAdapter
		// is also synchronized on the Trie to be serialized (such that no
		// modifications are allowed to the Trie while it's being serialized).
		// We are _not_ synchronizing around individual operations on the Trie
		// (e.g., a single get, put, remove, etc.) but rather the entire Trie
		// on each operation.
		synchronized(objects_) {
			return objects_.remove(key);
		}
	}
	
	public void deleteAllObjects() {
		// Explicitly synchronized around the objects Trie such that no
		// internal conflicts occur during GSON serialization (when GSON
		// is serializing this object to JSON).  NOTE: The TrieTypeAdapter
		// is also synchronized on the Trie to be serialized (such that no
		// modifications are allowed to the Trie while it's being serialized).
		// We are _not_ synchronizing around individual operations on the Trie
		// (e.g., a single get, put, remove, etc.) but rather the entire Trie
		// on each operation.
		synchronized(objects_) {
			objects_.clear();
		}
	}
	
	public HashedFileObject getObject(String key) {
		// Explicitly synchronized around the objects Trie such that no
		// internal conflicts occur during GSON serialization (when GSON
		// is serializing this object to JSON).  NOTE: The TrieTypeAdapter
		// is also synchronized on the Trie to be serialized (such that no
		// modifications are allowed to the Trie while it's being serialized).
		// We are _not_ synchronizing around individual operations on the Trie
		// (e.g., a single get, put, remove, etc.) but rather the entire Trie
		// on each operation.
		synchronized(objects_) {
			return objects_.get(key);
		}
	}
	
	public ObjectList startsWith(String prefix) {
		final ObjectList list = new ObjectList();
		// Explicitly synchronized around the objects Trie such that no
		// internal conflicts occur during GSON serialization (when GSON
		// is serializing this object to JSON).  NOTE: The TrieTypeAdapter
		// is also synchronized on the Trie to be serialized (such that no
		// modifications are allowed to the Trie while it's being serialized).
		// We are _not_ synchronizing around individual operations on the Trie
		// (e.g., a single get, put, remove, etc.) but rather the entire Trie
		// on each operation.
		synchronized(objects_) {
			final Map<String, HashedFileObject> startsWith =
				objects_.prefixMap(prefix);
			for(final Map.Entry<String, HashedFileObject> entry :
				startsWith.entrySet()) {
				final HashedFileObject hfo = entry.getValue();
				// Clone the underlying HFO for the return
				list.addObject(new HashedFileObject(
					//hfo.getFile(),
					hfo.getName(),
					hfo.getHeaders()));
			}
		}
		return list;
	}
	
	// Straight from Eclipse
	// Only uses the Repo ID
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((repoId_ == null) ? 0 : repoId_.hashCode());
		return result;
	}

	// Straight from Eclipse
	// Only uses the Repo ID
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Repository other = (Repository) obj;
		if (repoId_ == null) {
			if (other.repoId_ != null)
				return false;
		} else if (!repoId_.equals(other.repoId_))
			return false;
		return true;
	}
	
	public static final class TrieTypeAdapter
		implements JsonSerializer<Trie<String, HashedFileObject>>,
			JsonDeserializer<Trie<String, HashedFileObject>> {
		
		@Override
		public JsonElement serialize(final Trie<String, HashedFileObject> trie,
			final Type typeOfSrc, final JsonSerializationContext context) {
			final JsonObject json = new JsonObject();
			// Explicitly synchronized around the objects Trie such that no
			// internal conflicts occur during GSON serialization (when GSON
			// is serializing this object to JSON).  NOTE: The TrieTypeAdapter
			// is also synchronized on the Trie to be serialized (such that no
			// modifications are allowed to the Trie while it's being serialized).
			// We are _not_ synchronizing around individual operations on the Trie
			// (e.g., a single get, put, remove, etc.) but rather the entire Trie
			// on each operation.
			synchronized(trie) {
				for(final Map.Entry<String, HashedFileObject> entry :
					trie.entrySet()) {
					json.add(entry.getKey(),
						// The HashedFileObject of this entry. 
						context.serialize(entry.getValue(),
							HashedFileObject.class));
				}
			}
			return json;
		}
	
		@Override
		public Trie<String, HashedFileObject> deserialize(final JsonElement json,
			final Type typeOfT, final JsonDeserializationContext context) {
			if(!(json instanceof JsonObject)) {
				throw new JsonParseException("Dood! The Trie to " +
		    		"deserialize should be an object!");
			}
			final Trie<String, HashedFileObject> trie = new PatriciaTrie<>();
			for(final Map.Entry<String, JsonElement> entry :
				json.getAsJsonObject().entrySet()) {
				trie.put(entry.getKey(),
					// The HashedFileObject of this entry.
					context.<HashedFileObject>deserialize(entry.getValue(),
						HashedFileObject.class));
			}
			return trie;
		}
		
	}
	
	public static final class FileTypeAdapter
		implements JsonSerializer<File>, JsonDeserializer<File> {
		
		@Override
		public JsonElement serialize(final File src, final Type typeOfSrc, 
			final JsonSerializationContext context) {
			return new JsonPrimitive(src.getAbsolutePath());
		}
	
		@Override
		public File deserialize(final JsonElement json, final Type typeOfT, 
			final JsonDeserializationContext context) {
			return new File(json.getAsString());
		}
		
	}
	
} public class ObjectConflictException extends HavaloException {

	private static final long serialVersionUID = -7951798714052960609L;

	public ObjectConflictException(String message, Exception cause) {
		super(SC_CONFLICT, message, cause);
	}
	
	public ObjectConflictException(Exception cause) {
		super(SC_CONFLICT, cause);
	}
	
	public ObjectConflictException(String message) {
		super(SC_CONFLICT, message);
	}
	
}
 public class ObjectLengthNotSpecifiedException extends HavaloException {

	private static final long serialVersionUID = -9038601692753507142L;

	public ObjectLengthNotSpecifiedException(String message, Exception cause) {
		super(SC_LENGTH_REQUIRED, message, cause);
	}
	
	public ObjectLengthNotSpecifiedException(Exception cause) {
		super(SC_LENGTH_REQUIRED, cause);
	}
	
	public ObjectLengthNotSpecifiedException(String message) {
		super(SC_LENGTH_REQUIRED, message);
	}
	
}
 public class ObjectNotFoundException extends HavaloException {

	private static final long serialVersionUID = 5686431043127205040L;

	public ObjectNotFoundException(String message, Exception cause) {
		super(SC_NOT_FOUND, message, cause);
	}
	
	public ObjectNotFoundException(Exception cause) {
		super(SC_NOT_FOUND, cause);
	}
	
	public ObjectNotFoundException(String message) {
		super(SC_NOT_FOUND, message);
	}
	
}
 public class ObjectTooLargeException extends HavaloException {

	private static final long serialVersionUID = -9038601692753507142L;

	public ObjectTooLargeException(String message, Exception cause) {
		super(SC_BAD_REQUEST, message, cause);
	}
	
	public ObjectTooLargeException(Exception cause) {
		super(SC_BAD_REQUEST, cause);
	}
	
	public ObjectTooLargeException(String message) {
		super(SC_BAD_REQUEST, message);
	}
	
}
 public final class HavaloAuthenticationFilter implements CuracaoRequestFilter {

	private static final Logger logger__ =
        getLogger(HavaloAuthenticationFilter.class);

	public static final String HAVALO_AUTHENTICATION_ATTRIBUTE =
        "havalo.authentication";

	private static final String HAVALO_AUTHORIZATION_PREFIX = "Havalo ";
	private static final String HAVALO_AUTHORIZATION_SEPARATOR = ":";

	private final HavaloUserService userService_;

    @Injectable
	public HavaloAuthenticationFilter(final RepositoryManagerComponent component) {
		userService_ = new HavaloUserService(component.getRepositoryManager());
	}

    @Override
    public final void filter(final CuracaoContext context) throws Exception {
        try {
            final HttpServletRequest request = context.request_;
            // Extract the Authorization header from the incoming HTTP request.
            String header = request.getHeader(AUTHORIZATION);
            // If the header does not exist or does not start with the correct
            // token, give up immediately.
            if(header == null || !header.startsWith(HAVALO_AUTHORIZATION_PREFIX)) {
                throw new AuthenticationException("Request did not contain " +
                    "a valid '" + AUTHORIZATION + "' header.");
            }
            // Extract just the part of the Authorization header that follows
            // the Havalo authorization prefix.
            header = header.substring(HAVALO_AUTHORIZATION_PREFIX.length());
            final String[] tokens = header.split(HAVALO_AUTHORIZATION_SEPARATOR, 2);
            if(tokens == null || tokens.length != 2) {
                throw new AuthenticationException("Failed to extract correct " +
                    "number of tokens from '" + AUTHORIZATION + "' header.");
            }
            // If we get here, then we must have had some valid input
            // Authorization header with a real access key and signature.
            final String accessKey = tokens[0], signature = tokens[1];
            // request.getRequestURI();
            // Extract username from incoming signed request header.
            // Expected format is ...
            //    Authorization: Havalo AccessKey:Signature
            // ... where the AccessKey is the unique UUID used to identify the user.
            // And, the Signature is ...
            //    Base64( HMAC-SHA256( UTF-8-Encoding-Of( AccessSecret, StringToSign ) ) );
            // And, the StringToSign is ....
            //    HTTP-Verb (GET, PUT, POST, or DELETE) + "\n" +
            //    RFC822 Date (from 'Date' request header, must exist) + "\n" +
            //    Content-Type (from 'Content-Type' request header, optional) + "\n" +
            //    CanonicalizedResource (the part of this request's URL from
            //        the protocol name up to the query string in the first line
            //        of the HTTP request)
            // Call the user details service to load the user data for the UUID.
            final KeyPair userKp = userService_.loadKeyPairById(
                UUID.fromString(accessKey));
            if(userKp == null) {
                throw new AuthenticationException("User service returned " +
                    "null, which is an interface contract violation.");
            }
            // Get the string to sign -- will fail gracefully if the incoming
            // request does not have the proper headers attached to it.
            final String stringToSign = getStringToSign(request);
            // Compute the resulting signed signature.
            final String computed = HMACSHA256Signer.sign(userKp, stringToSign);
            // Does the signature match what was passed to us in the
            // Authorization request header?
            if(!computed.equals(signature)) {
                throw new BadCredentialsException("Signatures did not " +
                    "match (request=" + signature + ", computed=" + computed +
                    ")");
            }
            // Success!
            request.setAttribute(HAVALO_AUTHENTICATION_ATTRIBUTE, userKp);
        } catch (Exception e) {
            logger__.debug("Authentication failure; service failed " +
                "to authenticate request.", e);
            throw new AuthenticationException("Authentication " +
                "failed; either the provided signature did not match, " +
                "or you do not have permission to access the requested " +
                "resource.", e);
        }
    }
	
	private static final String getStringToSign(final HttpServletRequest request) {
		final StringBuilder sb = new StringBuilder();
		// HTTP-Verb (GET, PUT, POST, or DELETE) + "\n"
		sb.append(request.getMethod().toUpperCase()).append(LINE_SEPARATOR_UNIX);
		// RFC822 Date (from 'Date' request header, must exist) + "\n" +
		final String dateHeader;
		if((dateHeader = request.getHeader(DATE)) == null) {
			throw new BadCredentialsException("Incoming request missing " +
				"required '" + DATE + "' request header.");
		}
		sb.append(dateHeader).append(LINE_SEPARATOR_UNIX);
		// Content-Type (from 'Content-Type' request header, optional) + "\n" +
		final String contentType;
		if((contentType = request.getHeader(CONTENT_TYPE)) != null) {
			sb.append(contentType);
		}
		sb.append(LINE_SEPARATOR_UNIX);
		// CanonicalizedResource
		sb.append(request.getRequestURI());
		return sb.toString();
	}

    /**
	 * Computes an HMAC-SHA256 signature.
	 */
	private static final class HMACSHA256Signer {
		
		private static final String HMAC_SHA256_ALGORITHM_NAME = "HmacSHA256";
			
		/**
	     * Returns a Base-64 encoded HMAC-SHA256 signature.
	     */
		public static final String sign(final KeyPair kp,
                                        final String input) {
			try {
				// Get a new instance of the HMAC-SHA256 algorithm.
				final Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM_NAME);
				// Init it with our secret and the secret-key algorithm.
				mac.init(new SecretKeySpec(getBytesUtf8(kp.getSecret()),
					HMAC_SHA256_ALGORITHM_NAME));
				// Actually sign the input.
				return newStringUtf8(encodeBase64(mac.doFinal(
					getBytesUtf8(input))));
			} catch (Exception e) {
				throw new AuthenticationException("Failed to SHA-256 " +
					"sign input string: " + input, e);
			}
		}
		
	}

} public static final class ObjectKey {

    private final String decodedKey_;

    public ObjectKey(final String key) {
        notEmpty(key, "Key cannot be null or empty.");
        // URL-decode the incoming key (the name of the object)
        decodedKey_ = URLEncodingUtils.urlDecode(key);
    }

    public final String getDecodedKey() {
        return decodedKey_;
    }

    @Override
    public final String toString() {
        return decodedKey_;
    }

} public static final long getMaxUploadSize() {
	return getConfigInstance().getLong(
		HAVALO_UPLOAD_MAX_SIZE_PROPERTY);
}
import com.kolich.bolt.ReentrantReadWriteEntityLock;
import com.kolich.common.util.secure.KolichChecksum;
import curacao.annotations.Controller;
import curacao.annotations.Injectable;
import curacao.annotations.RequestMapping;
import curacao.annotations.parameters.convenience.ContentLength;
import curacao.annotations.parameters.convenience.ContentType;
import curacao.annotations.parameters.convenience.IfMatch;
import curacao.entities.CuracaoEntity;
import curacao.entities.empty.StatusCodeOnlyCuracaoEntity;
import curacao.mappers.request.matchers.CuracaoAntPathMatcher;
import com.kolich.havalo.components.RepositoryManagerComponent;
import com.kolich.havalo.controllers.HavaloApiController;
import com.kolich.havalo.entities.types.DiskObject;
import com.kolich.havalo.entities.types.HashedFileObject;
import com.kolich.havalo.entities.types.KeyPair;
import com.kolich.havalo.entities.types.Repository;
import com.kolich.havalo.exceptions.objects.ObjectConflictException;
import com.kolich.havalo.exceptions.objects.ObjectLengthNotSpecifiedException;
import com.kolich.havalo.exceptions.objects.ObjectNotFoundException;
import com.kolich.havalo.exceptions.objects.ObjectTooLargeException;
import com.kolich.havalo.filters.HavaloAuthenticationFilter;
import com.kolich.havalo.mappers.ObjectKeyArgumentMapper.ObjectKey;
import org.slf4j.Logger;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.io.Files.move;
import static com.google.common.net.HttpHeaders.*;
import static com.google.common.net.MediaType.OCTET_STREAM;
import static com.kolich.common.util.secure.KolichChecksum.getSHA1HashAndCopy;
import static curacao.annotations.RequestMapping.Method.*;
import static com.kolich.havalo.HavaloConfigurationFactory.getMaxUploadSize;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.apache.commons.io.IOUtils.copyLarge;
import static org.slf4j.LoggerFactory.getLogger;
/**
 * Copyright (c) 2015 Mark S. Kolich
 * http://mark.koli.ch
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.kolich.havalo.controllers.api;




@Controller
public class ObjectApi extends HavaloApiController {

    private static final Logger logger__ = getLogger(ObjectApi.class);

    private static final String OCTET_STREAM_TYPE = OCTET_STREAM.toString();

    private final long uploadMaxSize_;

    @Injectable
    public ObjectApi(final RepositoryManagerComponent component) {
        super(component.getRepositoryManager());
        uploadMaxSize_ = getMaxUploadSize();
    }

    @RequestMapping(methods=HEAD,
                    value="/api/object/{key}",
                    matcher=CuracaoAntPathMatcher.class,
                    filters=HavaloAuthenticationFilter.class)
    public final void head(final ObjectKey key,