/* ###
 * IP: GHIDRA
 *
 * 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 ghidra.server.remote;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.server.Unreferenced;
import java.util.*;

import db.buffers.LocalManagedBufferFile;
import db.buffers.RemoteManagedBufferFileHandle;
import ghidra.framework.remote.*;
import ghidra.framework.store.*;
import ghidra.server.Repository;
import ghidra.server.store.RepositoryFile;
import ghidra.server.store.RepositoryFolder;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.FileInUseException;

/**
 * <code>RepositoryHandleImpl</code> provides a Repository handle to a
 * remote user.
 */
public class RepositoryHandleImpl extends UnicastRemoteObject
		implements RemoteRepositoryHandle, Unreferenced {

	private volatile boolean isValid = true;
	private String repoName;
	private boolean clientActive = true;
	private String currentUser;
	private Repository repository;
	private HashMap<String, ItemCheckoutStatus> transientCheckouts;
	private Object syncObject;

	private LinkedList<RepositoryChangeEvent> eventQueue = new LinkedList<RepositoryChangeEvent>();

	/**
	 * Construct a repository handle for a specific user.
	 * @param user user which corresponds to client
	 * @param repository repository store to be wrapped
	 * @throws RemoteException if failed to instantiate remote object
	 */
	RepositoryHandleImpl(String user, Repository repository) throws RemoteException {
		super(ServerPortFactory.getRMISSLPort(), GhidraServer.getRMIClientSocketFactory(),
			GhidraServer.getRMIServerSocketFactory());
		this.currentUser = user;
		this.repository = repository;
		this.syncObject = repository.getSyncObject();
		this.repoName = repository.getName();
		repository.log(null, "Generated handle", user);
		repository.addHandle(this);
	}

	public Repository getRepository() {
		return repository;
	}

	/**
	 * RMI callback when instance becomes unreferenced by any remote client
	 */
	@Override
	public void unreferenced() {
		dispose();
	}

	/**
	 * Dispose handle
	 */
	public void dispose() {
		synchronized (syncObject) {
			if (!isValid) {
				return;
			}
			terminateTransientCheckouts();
			repository.log(null, "Handle disposed", currentUser);
			if (eventQueue != null) {
				synchronized (eventQueue) {
					eventQueue.clear();
					eventQueue.notifyAll();
				}
			}
			try {
				unexportObject(this, true);
			}
			catch (NoSuchObjectException e) {
				// ignore
			}
			repository.dropHandle(this);
			RemoteBufferFileImpl.dispose(this);
			currentUser = null;
			isValid = false;
		}
	}

	private void terminateTransientCheckouts() {
		if (transientCheckouts == null || transientCheckouts.isEmpty()) {
			return;
		}
		try {
			repository.log(null, "Clearing " + transientCheckouts.size() + " transiet checkouts",
				currentUser);

			ArrayList<String> pathnames = new ArrayList<>(transientCheckouts.keySet());
			for (String pathname : pathnames) {
				int index = pathname.lastIndexOf(FileSystem.SEPARATOR_CHAR);
				String parentPath = FileSystem.SEPARATOR;
				if (index != 0) {
					parentPath = pathname.substring(0, index);
				}
				String itemName = pathname.substring(index + 1);

				ItemCheckoutStatus transientCheckout = transientCheckouts.get(pathname);
				if (transientCheckout != null) {
					// Since dropped transient checkouts could occur in large volume due to headless
					// processing, don't bother sending notification
					terminateCheckout(parentPath, itemName, transientCheckout.getCheckoutId(),
						false);
				}
			}
		}
		catch (IOException e) {
			String msg = e.getMessage();
			if (msg == null) {
				msg = e.toString();
			}
			msg = "Failed to cleanup transient checkouts - server restart may be required: " + msg;
			repository.log(null, msg, currentUser);
		}
	}

	private void addTransientCheckout(String pathname, ItemCheckoutStatus checkoutStatus) {
		if (transientCheckouts == null) {
			transientCheckouts = new HashMap<String, ItemCheckoutStatus>();
		}
		transientCheckouts.put(pathname, checkoutStatus);
	}

	private void removeTransientCheckout(String pathname, long checkoutId) {
		if (transientCheckouts == null) {
			return;
		}
		ItemCheckoutStatus transientCheckout = transientCheckouts.get(pathname);
		if (transientCheckout != null && transientCheckout.getCheckoutId() == checkoutId) {
			transientCheckouts.remove(pathname);
		}
	}

	private void validate() throws RemoteException {
		if (!isValid) {
			throw new RemoteException("bad repository handle");
		}
	}

	/**
	 * Post repository change events to the client.
	 * @param events change events
	 */
	public void dispatchEvents(RepositoryChangeEvent[] events) {
		synchronized (eventQueue) {
			if (!isValid) {
				return;
			}
			for (RepositoryChangeEvent event : events) {
				eventQueue.addLast(event);
			}
			eventQueue.notifyAll();
		}
	}

	/**
	 * Verify that the client is active and continuing to read events
	 */
	public void checkHandle() {
		if (!isValid) {
			return;
		}

		RepositoryChangeEvent openFileCountEvent =
			new RepositoryChangeEvent(RepositoryChangeEvent.REP_OPEN_HANDLE_COUNT, null, null, null,
				Integer.toString(RemoteBufferFileImpl.getOpenFileCount(this)));

		synchronized (eventQueue) {
			if (clientActive) {
				clientActive = false;
				if (eventQueue.isEmpty()) {

					//eventQueue.add(0, NULL_EVENT);

					// Send open-file count periodically instead of null event
					eventQueue.add(openFileCountEvent);

					eventQueue.notifyAll();
				}
				return;
			}
		}
		repository.log(null, "Not listening (may be sleeping)!", currentUser);
		dispose();
	}

	@Override
	public RepositoryChangeEvent[] getEvents() throws IOException {
		synchronized (eventQueue) {
			clientActive = true;
			if (eventQueue.isEmpty()) {
				try {
					eventQueue.wait();
				}
				catch (InterruptedException e) {
					throw new IOException("Event wait cancelled");
				}
			}
			if (eventQueue.isEmpty()) {
				throw new IOException("Handle disposed by server");
			}
			RepositoryChangeEvent[] events = new RepositoryChangeEvent[eventQueue.size()];
			eventQueue.toArray(events);
			eventQueue.clear();
			return events;
		}
	}

	@Override
	public void close() {
		dispose();
	}

	@Override
	public String getName() throws RemoteException {
		synchronized (syncObject) {
			validate();
			return repository.getName();
		}
	}

	@Override
	public User[] getUserList() throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				return repository.getUserList(currentUser);
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName, null,
					"Get repository user list", currentUser);
			}
		}
	}

	@Override
	public boolean anonymousAccessAllowed() throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				return repository.anonymousAccessAllowed();
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName, null, null, currentUser);
			}
		}
	}

	@Override
	public void setUserList(User[] users, boolean anonymousAccessAllowed) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.setUserList(currentUser, users, anonymousAccessAllowed);
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName, null,
					"Set repository user list", currentUser);
			}
		}
	}

	@Override
	public User getUser() throws RemoteException {
		synchronized (syncObject) {
			validate();
			return repository.getUser(currentUser);
		}
	}

	public String getUserName() {
		return currentUser;
	}

	@Override
	public String[] getServerUserList() throws RemoteException {
		synchronized (syncObject) {
			validate();
			return repository.getServerUserList(currentUser);
		}
	}

	@Override
	public String[] getSubfolderList(String folderPath) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFolder folder;
				try {
					folder = repository.getFolder(currentUser, folderPath, false);
				}
				catch (InvalidNameException e) {
					throw new AssertException();
				}
				if (folder == null) {
					return new String[0];
				}
				RepositoryFolder[] subfolders = folder.getFolders();
				String[] subfolderNames = new String[subfolders.length];
				for (int i = 0; i < subfolders.length; i++) {
					subfolderNames[i] = subfolders[i].getName();
				}
				return subfolderNames;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName, folderPath,
					"Get subfolder list", currentUser);
			}
		}
	}

	@Override
	public int getItemCount() throws UnsupportedOperationException, IOException {
		synchronized (syncObject) {
			validate();
			try {
				return repository.getItemCount();
			}
			catch (UnsupportedOperationException uoe) {
				throw uoe; // Not supported by MangledFileSystem
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName, null,
					"Get item count", currentUser);
			}
		}
	}

	@Override
	public RepositoryItem[] getItemList(String folderPath) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFolder folder = repository.getFolder(currentUser, folderPath, false);
				if (folder == null) {
					return new RepositoryItem[0];
				}
				RepositoryFile[] files = folder.getFiles();
				RepositoryItem[] items = new RepositoryItem[files.length];
				for (int i = 0; i < files.length; i++) {
					items[i] = files[i].getItem();
				}
				return items;
			}
			catch (InvalidNameException e) {
				throw new AssertException();
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName, folderPath,
					"Get item list", currentUser);
			}
		}
	}

	@Override
	public RepositoryItem getItem(String folderPath, String name) throws IOException {
		synchronized (syncObject) {
			validate();
			RepositoryFile rf = getFile(folderPath, name);
			return rf != null ? rf.getItem() : null;
		}
	}

	@Override
	public RepositoryItem getItem(String fileID) throws IOException {
		throw new UnsupportedOperationException("getItem by File-ID");
	}

	private RepositoryFile getFile(String parentPath, String itemName) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFolder folder = repository.getFolder(currentUser, parentPath, false);
				if (folder != null) {
					RepositoryFile rf = folder.getFile(itemName);
					if (rf != null) {
						return rf;
					}
				}
			}
			catch (InvalidNameException e) {
				throw new AssertException();
			}
			return null;
		}
	}

	@Override
	public void createTextDataFile(String parentPath, String itemName, String fileID,
			String contentType, String textData, String comment)
			throws InvalidNameException, IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true);
				if (folder == null) {
					throw new IOException("Failed to create repository Folder " + parentPath);
				}
				folder.createTextDataFile(itemName, fileID, contentType, textData, comment,
					currentUser);
			}
			catch (InvalidNameException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Create text item", currentUser);
			}
		}
	}

	@Override
	public RemoteManagedBufferFileHandle createDatabase(String parentPath, String itemName,
			String fileID, int bufferSize, String contentType, String projectPath)
			throws InvalidNameException, IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true);
				if (folder == null) {
					throw new IOException("Failed to create repository Folder " + parentPath);
				}
				LocalManagedBufferFile bf = folder.createDatabase(itemName, fileID, bufferSize,
					contentType, currentUser, projectPath);
				return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName));
			}
			catch (InvalidNameException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Create database item", currentUser);
			}
		}
	}

	@Override
	public RemoteManagedBufferFileImpl openDatabase(String parentPath, String itemName, int version,
			int minChangeDataVer) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				LocalManagedBufferFile bf = rf.openDatabase(version, minChangeDataVer, currentUser);
				return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName));
			}
			catch (FileNotFoundException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName) + "@" + version,
					"Open database version", currentUser);
			}
		}
	}

	@Override
	public RemoteManagedBufferFileImpl openDatabase(String parentPath, String itemName,
			long checkoutId) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				LocalManagedBufferFile bf = rf.openDatabase(checkoutId, currentUser);
				return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName));
			}
			catch (FileNotFoundException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName) + ":0x" + Long.toHexString(checkoutId),
					"Open database for checkin", currentUser);
			}
		}
	}

	@Override
	public Version[] getVersions(String parentPath, String itemName) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				return rf.getVersions(currentUser);
			}
			catch (FileNotFoundException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Get versions", currentUser);
			}
		}
	}

	@Override
	public void deleteItem(String parentPath, String itemName, int version) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				checkFileInUse(parentPath, itemName);
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf != null) {
					rf.delete(version, currentUser);
				}
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName) + "@" + version,
					"Delete item version", currentUser);
			}
		}
	}

	@Override
	public void moveFolder(String oldParentPath, String newParentPath, String oldFolderName,
			String newFolderName) throws InvalidNameException, IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				checkFolderInUse(oldParentPath, oldFolderName);
				RepositoryFolder folder = repository.getFolder(currentUser,
					oldParentPath + FileSystem.SEPARATOR + oldFolderName, false);
				RepositoryFolder newParent = repository.getFolder(currentUser, newParentPath, true);
				if (folder != null) {
					folder.moveTo(newParent, newFolderName, currentUser);
				}
			}
			catch (InvalidNameException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(oldParentPath, oldFolderName),
					"Move folder to: " + getPathname(newParentPath, newFolderName), currentUser);
			}
		}
	}

	@Override
	public void moveItem(String oldParentPath, String newParentPath, String oldItemName,
			String newItemName) throws InvalidNameException, IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				checkFileInUse(oldParentPath, oldItemName);
				RepositoryFile rf = getFile(oldParentPath, oldItemName);
				if (rf == null) {
					throw new FileNotFoundException(oldItemName + " not found in repository");
				}
				RepositoryFolder folder = repository.getFolder(currentUser, newParentPath, true);
				if (folder == null) {
					throw new IOException("Failed to create repository Folder " + newParentPath);
				}
				rf.moveTo(folder, newItemName, currentUser);
			}
			catch (InvalidNameException e) {
				throw e;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(oldParentPath, oldItemName),
					"Move item to: " + getPathname(newParentPath, newItemName), currentUser);
			}
		}
	}

	private void checkFileInUse(String parentPath, String itemName) throws FileInUseException {
		String[] openFileUsers = RemoteBufferFileImpl.getOpenFileUsers(repository.getName(),
			getPathname(parentPath, itemName));
		if (openFileUsers != null) {
			StringBuffer buf = new StringBuffer("");
			for (String user : openFileUsers) {
				if (buf.length() != 0) {
					buf.append(", ");
				}
				buf.append(user);
			}
			throw new FileInUseException(itemName + " in use by: " + buf.toString());
		}
	}

	private void checkFolderInUse(String parentPath, String folderName) throws IOException {
		RepositoryFolder folder;
		try {
			folder = repository.getFolder(currentUser, getPathname(parentPath, folderName), false);
			if (folder == null) {
				return;
			}
			if (isFolderInUse(folder)) {
				throw new FileInUseException("Repository folder " + folderName +
					" contains one or more files that are checked-out by one or more users.");
			}
		}
		catch (InvalidNameException e) {
			// ignore
		}
	}

	private boolean isFolderInUse(RepositoryFolder folder) throws IOException {
		for (RepositoryFolder f : folder.getFolders()) {
			if (isFolderInUse(f)) {
				return true;
			}
		}
		for (RepositoryFile rf : folder.getFiles()) {
			if (rf.hasCheckouts()) {
				return true;
			}
			String[] openFileUsers = RemoteBufferFileImpl.getOpenFileUsers(repository.getName(),
				getPathname(folder.getPathname(), rf.getName()));
			if (openFileUsers != null) {
				return true;
			}
		}
		return false;
	}

	@Override
	public ItemCheckoutStatus checkout(String parentPath, String itemName,
			CheckoutType checkoutType, String projectPath) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				ItemCheckoutStatus checkoutStatus =
					rf.checkout(checkoutType, currentUser, projectPath);
				if (checkoutStatus != null &&
					checkoutStatus.getCheckoutType() == CheckoutType.TRANSIENT) {
					addTransientCheckout(rf.getPathname(), checkoutStatus);
				}
				return checkoutStatus;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Checkout item (" + checkoutType + ") to " + projectPath, currentUser);
			}
		}
	}

	@Override
	public void updateCheckoutVersion(String parentPath, String itemName, long checkoutId,
			int checkoutVersion) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				repository.validateWritePrivilege(currentUser);
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf != null) {
					rf.updateCheckoutVersion(checkoutId, checkoutVersion, currentUser);
				}
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Update checkout 0x" + Long.toHexString(checkoutId) + " to version " +
						checkoutVersion,
					currentUser);
			}
		}
	}

	@Override
	public void terminateCheckout(String parentPath, String itemName, long checkoutId,
			boolean notify) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf != null) {
					rf.terminateCheckout(checkoutId, currentUser, notify);
					removeTransientCheckout(rf.getPathname(), checkoutId);
				}
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Terminate checkout 0x" + Long.toHexString(checkoutId),
					currentUser);
			}
		}
	}

	@Override
	public ItemCheckoutStatus getCheckout(String parentPath, String itemName, long checkoutId)
			throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				return rf.getCheckout(checkoutId, currentUser);
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Get checkout status 0x" + Long.toHexString(checkoutId),
					currentUser);
			}
		}
	}

	@Override
	public ItemCheckoutStatus[] getCheckouts(String parentPath, String itemName)
			throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				return rf.getCheckouts(currentUser);
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Get checkout list", currentUser);
			}
		}
	}

	@Override
	public boolean folderExists(String folderPath) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				return (repository.getFolder(currentUser, folderPath, false) != null);
			}
			catch (InvalidNameException e) {
				throw new AssertException();
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					folderPath, "Check folder", currentUser);
			}
		}
	}

	@Override
	public boolean fileExists(String parentPath, String itemName) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf;
				try {
					rf = getFile(parentPath, itemName);
				}
				catch (FileNotFoundException e) {
					return false;
				}
				return rf != null;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Check item", currentUser);
			}
		}
	}

	@Override
	public long getLength(String parentPath, String itemName) throws IOException {
		synchronized (syncObject) {
			validate();
			RepositoryFile rf;
			try {
				rf = getFile(parentPath, itemName);
				if (rf == null) {
					return 0;
				}
				return rf.length();
			}
			catch (FileNotFoundException e) {
				return 0;
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Get item length", currentUser);
			}
		}
	}

	@Override
	public boolean hasCheckouts(String parentPath, String itemName) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				return rf.hasCheckouts();
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Has checkouts", currentUser);
			}
		}
	}

	@Override
	public boolean isCheckinActive(String parentPath, String itemName) throws IOException {
		synchronized (syncObject) {
			validate();
			try {
				RepositoryFile rf = getFile(parentPath, itemName);
				if (rf == null) {
					throw new FileNotFoundException(itemName + " not found in repository");
				}
				return rf.isCheckinActive();
			}
			catch (Throwable t) {
				throw RemoteExceptionUtil.dispatchIOException(t, repoName,
					getPathname(parentPath, itemName),
					"Is checkin active", currentUser);
			}
		}
	}

	private static String getPathname(String parentPath, String itemName) {
		StringBuffer path = new StringBuffer(parentPath);
		if (path.charAt(path.length() - 1) != FileSystem.SEPARATOR_CHAR) {
			path.append(FileSystem.SEPARATOR_CHAR);
		}
		path.append(itemName);
		return path.toString();
	}

}
