/*
 * $Id: PseudoFileSemaphore.java,v 0.0 2009/04/02 00:00:00 t-imamura Exp $
 *
 * Copyright (c) 2009 t-imamura, All rights reserved.
 */
package semaphore;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.regex.Pattern;

/**
 * ファイルを利用した擬似セマフォ.
 * <br>
 * システムの一時ファイルディレクトリにロックファイルを作成し、
 * バイト単位でロックをしてパーミット数を管理します。
 *
 * <pre>
 * final PseudoFileSemaphore semaphore = new PseudoFileSemaphore(SEMAPHORE_NAME, PERMITS);
 *
 * // シャットダウンフック
 * Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
 *   public void run() {
 *     semaphore.release();
 *   }
 * }));
 *
 * // パーミットの取得を試みる
 * if (semaphore.acquire()) {
 *   // 起動OK!
 *   JOptionPane.showMessageDialog(null, "アプリケーションを起動しました。",
 *       SEMAPHORE_NAME, JOptionPane.INFORMATION_MESSAGE);
 * } else {
 *   // 起動NG!
 *   JOptionPane.showMessageDialog(null, "アプリケーションの起動上限に達しました。",
 *       SEMAPHORE_NAME, JOptionPane.ERROR_MESSAGE);
 * }
 * </pre>
 */
public class PseudoFileSemaphore {

	/** ロックファイル拡張子 */
	private static final String LOCK_FILE_EXT = ".lck";

	/** 利用可能なパーミットの数 */
	private final int permits;

	/** ロックファイル */
	private final File file;

	/** ロックファイルチャネル */
	private FileChannel channel;

	/** ファイルロック */
	private FileLock filelock;

	/**
	 * PseudoFileSemaphoreを構築する。
	 * <br>
	 * <code>name</code> には、英数といくつかの記号(<code>-+=_(){}</code>)のみが利用できます。
	 *
	 * @param name アプリケーション(ロックファイル)名
	 * @param permits 利用可能なパーミットの数
	 */
	public PseudoFileSemaphore(String name, int permits) {
		if (!Pattern.compile("^[-+=0-9A-Za-z_(){}]+$").matcher(name).matches()) {
			// ファイル名として使える文字なら何でも良いが、無難な英数といくつかの記号のみ
			throw new IllegalArgumentException("name cannot be used. name=" + name);
		}
		if(permits < 1){
			throw new IllegalArgumentException("permits is small. permits=" + permits);
		}

		this.permits = permits;
		this.file = new File(System.getProperty("java.io.tmpdir"), name + LOCK_FILE_EXT);
	}

	/**
	 * 一時ファイルディレクトリにロックファイルを作成する。
	 *
	 * @param file ロックファイルパス
	 * @param permits 最大取得可能上限
	 * @return ロックファイルチャネル
	 * @throws IOException 入出力エラーが発生した場合
	 */
	private FileChannel createLockFileChannel(File file, int permits) throws IOException {
		RandomAccessFile raf = new RandomAccessFile(file, "rw");
		FileChannel channel = raf.getChannel();
		raf.setLength(permits);
		if (raf.length() < permits) {
			raf.seek(raf.length());
			for (long i = 0; i < permits - raf.length(); i++) {
				raf.write(0xff);
			}
		} else if (raf.length() > permits) {
			channel.truncate(permits);
		}
		return channel;
	}

	/**
	 * パーミットを解放し、セマフォーに戻します。
	 */
	public synchronized void release() {
		if (this.filelock == null)
			return;
		try {
			this.filelock.release();
			this.filelock = null;
			this.channel.close();
			this.channel = null;
			this.file.delete();
		} catch (IOException e) {
			// 例外は特に何もしない
		}
	}

	/**
	 * 利用可能な場合に限り、このセマフォーからパーミットを取得します。
	 *
	 * @return パーミットが取得された場合は <code>true</code>
	 */
	public synchronized boolean acquire() {
		try {
			FileChannel channel = createLockFileChannel(this.file, this.permits);
			FileLock lock = null;
			for (int p = 0; p < this.permits; p++) {
				try {
					lock = channel.tryLock(p, 1, false);
					if (lock != null && lock.isValid()) {
						break;
					} else {
						lock = null;
					}
				} catch (OverlappingFileLockException e) {
					// 既にロックされてたら次を試みる
					continue;
				}
			}
			if (lock != null) {
				this.filelock = lock;
				this.channel = channel;
			} else if(channel != null) {
				channel.close();
			}
		} catch (IOException e) {
			// 例外が発生しても何もしない、パーミットが取れないだけ。
		}
		return (this.filelock != null);
	}

}
