/*******************************************************************************
* Copyright (c) 2006-2010 eBay Inc. All Rights Reserved.
* 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
*******************************************************************************/
/**
*
*/
package org.ebayopensource.turmeric.tools.errorlibrary;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import javax.xml.bind.JAXB;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.ebayopensource.turmeric.runtime.common.impl.utils.CallTrackingLogger;
import org.ebayopensource.turmeric.runtime.common.impl.utils.LogManager;
import org.ebayopensource.turmeric.tools.codegen.util.CodeGenUtil;
import org.ebayopensource.turmeric.tools.errorlibrary.exception.ErrorIdGeneratorException;
import org.ebayopensource.turmeric.common.config.OrganizationErrorBlocks;
import org.ebayopensource.turmeric.common.config.ReservedErrorBlock;
import org.ebayopensource.turmeric.common.config.UsedErrorBlock;
import org.ebayopensource.turmeric.common.config.OrganizationErrorBlocks.UsedErrorBlockList;
import org.ebayopensource.turmeric.common.config.OrganizationErrorBlocks.ReservedErrorBlockList;
/**
* @author arajmony,stecheng
*
*/
class FileErrorIdGenerator implements ErrorIdGenerator {
private JAXBContext jc;
private String m_fileName;
private Unmarshaller u;
private Marshaller m;
private OrganizationErrorBlocks errorBlocks;
private static final String s_lockFileExtension = ".lck";
public static int MINIMUM_BLOCKSIZE = 100;
public static int DEFAULT_BLOCKSIZE = 1000;
private int blocksize = DEFAULT_BLOCKSIZE;
private int SOA_RESERVED_BLOCK = 100000;
private Map<String,List<UsedErrorBlock>> usedErrorBlockMap;
private Set<Range> allocatedRanges = new TreeSet<Range>();
private static CallTrackingLogger s_Logger = LogManager.getInstance(FileErrorIdGenerator.class);
/**
* Builder class helps solve the telescoping constructor problem
*/
static class Builder implements ErrorIdGenerator.Builder {
private String storeLocation;
private String organizationName;
private int blocksize = DEFAULT_BLOCKSIZE;
private static final String SUFFIX_FOR_FILE_NAME = "ErrorIDs.xml";
/**
* This is the equivalent to the filename where error ids are maintained
* @param storeLocation
* @return this
*/
public Builder storeLocation( String storeLocation ) {
this.storeLocation = storeLocation;
return this;
}
public Builder organizationName( String organizationName ) {
this.organizationName = organizationName;
return this;
}
public Builder blocksize( int blocksize ) {
this.blocksize = blocksize; return this;
}
/**
* This implementation does not use authentication to gain access to the file.
* It is assumed that all users have the same access rights to the file.
* @throws UnsupportedOperationException
*/
public ErrorIdGenerator.Builder credentials( String username, String password ) {
throw new UnsupportedOperationException();
}
/**
* @throws IllegalArgumentException
* if fileName is not specified or
* if organization is not specified or does NOT match the organization contained within fileName
* @throws IllegalStateException if there were problems loading/verifying fileName
*/
public ErrorIdGenerator build() {
validateArguments();
String fileName = CodeGenUtil.toOSFilePath(storeLocation) + organizationName + SUFFIX_FOR_FILE_NAME;
return new FileErrorIdGenerator( fileName, organizationName, blocksize );
}
private void validateArguments() {
if ( CodeGenUtil.isEmptyString(storeLocation))
throw new IllegalArgumentException( "No filename(store Location) specified!" );
if ( CodeGenUtil.isEmptyString(organizationName) )
throw new IllegalArgumentException( "No organizationName specified!" );
File storeLocationFile = new File(storeLocation);
if(!storeLocationFile.exists())
throw new IllegalArgumentException( "The specified location for storeLocation does not exist : "+ storeLocation);
if(!storeLocationFile.isDirectory())
throw new IllegalArgumentException( "The specified location for storeLocation is not a directory : "+ storeLocation);
if(!storeLocationFile.canWrite())
throw new IllegalArgumentException( "The specified location for storeLocation is not writable : "+ storeLocation);
if(blocksize < MINIMUM_BLOCKSIZE)
throw new IllegalArgumentException( "The block specified is less then the minimum block size of "+ MINIMUM_BLOCKSIZE);
}
}
/**
*
* @param fileNameParam
* @param organization
* @param blocksize
* @throws IllegalStateException if there were problems loading/verifying fileName
*/
private FileErrorIdGenerator( String fileNameParam, String organization, int blocksize ) {
this.blocksize = blocksize;
try {
jc = JAXBContext.newInstance( OrganizationErrorBlocks.class );
u = jc.createUnmarshaller();
m = jc.createMarshaller();
m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE );
this.m_fileName = fileNameParam;
File xmlFile = new File( fileNameParam );
if ( xmlFile.exists() )
errorBlocks = (OrganizationErrorBlocks) u.unmarshal( xmlFile );
else
errorBlocks = buildDefaultErrorBlock( organization );
if ( !errorBlocks.getOrganization().equals( organization ) )
throw new IllegalArgumentException( "The specified organization name is inconsistent with the name stored in the file: " + errorBlocks.getOrganization() );
} catch ( JAXBException e ) {
/**
* Don't let the JAXBException leak. This allows our choice of XML bindings to change in the future
*/
throw new IllegalStateException( "Failed to initialize the environment or load the file: " + e.getMessage() );
}
buildUsedErrorBlockMap();
buildAllocatedRanges();
try {
persist();
} catch (ErrorIdGeneratorException e) {
s_Logger.log(Level.WARNING, "Unable to persist", e);
}
}
private void buildUsedErrorBlockMap() {
usedErrorBlockMap = new HashMap<String,List<UsedErrorBlock>>();
for ( UsedErrorBlock usedErrorBlock : errorBlocks.getUsedErrorBlockList().getUsedErrorBlock() ) {
List<UsedErrorBlock> domainErrorBlockList = usedErrorBlockMap.get( usedErrorBlock.getDomain() );
if ( domainErrorBlockList == null ) {
domainErrorBlockList = new ArrayList<UsedErrorBlock>();
usedErrorBlockMap.put( usedErrorBlock.getDomain(), domainErrorBlockList );
}
domainErrorBlockList.add( usedErrorBlock );
}
}
private void buildAllocatedRanges() {
allocatedRanges = new TreeSet<Range>();
// TODO: Verify no overlapping ranges
// TODO: Verify that start < end
for ( ReservedErrorBlock reservedErrorBlock : errorBlocks.getReservedErrorBlockList().getReservedErrorBlock() ) {
allocatedRanges.add( new Range( reservedErrorBlock.getStart(), reservedErrorBlock.getEnd() ) );
}
for ( UsedErrorBlock usedErrorBlock : errorBlocks.getUsedErrorBlockList().getUsedErrorBlock() ) {
allocatedRanges.add( new Range( usedErrorBlock.getStart(), usedErrorBlock.getEnd() ) );
}
}
// private UsedErrorBlock findLastUsedErrorBlock() {
// List<UsedErrorBlock> usedErrorBlockList = errorBlocks.getUsedErrorBlockList().getUsedErrorBlock();
// int indexOfLastUsedBlock = usedErrorBlockList.size() - 1;
// return usedErrorBlockList.get( indexOfLastUsedBlock );
// }
private Range findNextUnallocatedRange() {
Range unallocatedRange = new Range( 1, blocksize - 1 );
for ( Range allocatedRange : allocatedRanges ) {
if ( !allocatedRange.intersects( unallocatedRange ) )
break;
unallocatedRange = new Range( allocatedRange.getEnd() + 1, allocatedRange.getEnd() + blocksize );
}
return unallocatedRange;
}
private OrganizationErrorBlocks buildDefaultErrorBlock( String organization ) {
errorBlocks = new OrganizationErrorBlocks();
errorBlocks.setDefaultBlockSize( DEFAULT_BLOCKSIZE );
errorBlocks.setOrganization( organization );
errorBlocks.setReservedErrorBlockList(getDefaultReservedBlockList());// new ReservedErrorBlockList() );
errorBlocks.setUsedErrorBlockList( new UsedErrorBlockList() );
return errorBlocks;
}
// private UsedErrorBlock buildUsedErrorBlock( String domain, long start, long end ) {
// UsedErrorBlock usedErrorBlock = new UsedErrorBlock();
// usedErrorBlock.setDomain( domain );
// usedErrorBlock.setStart( start );
// usedErrorBlock.setEnd( start + blocksize );
// usedErrorBlock.setLast( start );
// return usedErrorBlock;
// }
private ReservedErrorBlockList getDefaultReservedBlockList() {
ReservedErrorBlockList reservedErrorBlockList = new ReservedErrorBlockList();
long reservedBlockStart = 1;
ReservedErrorBlock soaReservedErrorBlock = new ReservedErrorBlock();
soaReservedErrorBlock.setStart(reservedBlockStart);
soaReservedErrorBlock.setEnd(reservedBlockStart + SOA_RESERVED_BLOCK - 1);
reservedErrorBlockList.getReservedErrorBlock().add(soaReservedErrorBlock);
return reservedErrorBlockList;
}
private boolean isDomainCreated( String domain ) {
return usedErrorBlockMap.containsKey( domain );
}
/**
*
* @param domain
* @throws IllegalArgumentException if the blocksize is not valid
*/
private void createDomain( String domain ) {
if ( blocksize < MINIMUM_BLOCKSIZE )
throw new IllegalArgumentException( "Block size must be at least " + MINIMUM_BLOCKSIZE );
Range range = findNextUnallocatedRange();
allocatedRanges.add( range );
UsedErrorBlock usedErrorBlock = new UsedErrorBlock();
usedErrorBlock.setDomain( domain );
usedErrorBlock.setStart( range.getStart() );
usedErrorBlock.setEnd( range.getEnd() );
usedErrorBlock.setLast( range.getStart() - 1 );
List<UsedErrorBlock> usedErrorBlockList = errorBlocks.getUsedErrorBlockList().getUsedErrorBlock();
usedErrorBlockList.add( usedErrorBlock );
List<UsedErrorBlock> domainErrorBlockList = usedErrorBlockMap.get( usedErrorBlock.getDomain() );
if ( domainErrorBlockList == null ) {
domainErrorBlockList = new ArrayList<UsedErrorBlock>();
usedErrorBlockMap.put( usedErrorBlock.getDomain(), domainErrorBlockList );
}
domainErrorBlockList.add( usedErrorBlock );
}
private UsedErrorBlock getUsedErrorBlock( String domain ) {
List<UsedErrorBlock> domainErrorBlockList = usedErrorBlockMap.get( domain );
for ( UsedErrorBlock usedErrorBlock : domainErrorBlockList )
if ( usedErrorBlock.getLast() < usedErrorBlock.getEnd() )
return usedErrorBlock;
/**
* Otherwise the domain ran out of error id's in which case, we create a new block of them
*/
createDomain( domain );
domainErrorBlockList = usedErrorBlockMap.get( domain );
assert domainErrorBlockList != null;
assert domainErrorBlockList.size() > 1;
return domainErrorBlockList.get( domainErrorBlockList.size() - 1 );
}
/**
*
*/
public long getNextId(String domain) throws IllegalArgumentException, IllegalStateException, ErrorIdGeneratorException {
long nextId;
synchronized (this) {
File xmlFile = new File( m_fileName );
if ( xmlFile.exists() ){
errorBlocks = JAXB.unmarshal(xmlFile, OrganizationErrorBlocks.class);
buildUsedErrorBlockMap();
buildAllocatedRanges();
}
boolean isFileLock = tryGetFileLock();
if(!isFileLock)
throw new ErrorIdGeneratorException("Could not get the lock for the file : " + m_fileName);
if ( !isDomainCreated( domain ) )
createDomain( domain );
nextId = findAndUpdateNextId( domain );
persist();
}
return nextId;
}
// private void updateLastId( long nextId, String domain ) {
// UsedErrorBlock usedErrorBlock = getUsedErrorBlock( domain );
// usedErrorBlock.setLast( nextId );
// }
private boolean tryGetFileLock() throws ErrorIdGeneratorException{
/*
* a. create the lock file it it does not exist, it it exists return
* b. create a copy of the exisiting file
* c. delete the exisisting file
* d. rename the new file
* e. take the lock on the new file
*/
File file = new File(m_fileName);
String lockFilePath = getLockFilesPath();
File lockFile = new File(lockFilePath);
if(lockFile.exists()){
s_Logger.log(Level.SEVERE, "#1 Could not get lock for the file : " + m_fileName );
throw new ErrorIdGeneratorException("#1 Could not get lock for the file : " + m_fileName );
}else{
try {
if(!lockFile.createNewFile()){
String errMsg = "#1 Could not create the lock file : " + m_fileName ;
s_Logger.log(Level.SEVERE,errMsg );
throw new ErrorIdGeneratorException(errMsg );
}
} catch (IOException e) {
String errMsg = e.getMessage();
s_Logger.log(Level.SEVERE,errMsg );
throw new ErrorIdGeneratorException(errMsg );
}
}
FileOutputStream tempStream = null;
try {
File tempFile = new File(m_fileName + ".copy");
if(!copyFile(file,tempFile)){
String errMsg = "#1 Could not copy the original file to temp file: " + m_fileName ;
s_Logger.log(Level.SEVERE,errMsg );
throw new ErrorIdGeneratorException(errMsg );
}
if(!file.delete()){
String errMsg = "#1 Could not delete the file : " + m_fileName;
s_Logger.log(Level.SEVERE, errMsg );
throw new ErrorIdGeneratorException(errMsg);
}
file = new File(m_fileName);
if(!tempFile.renameTo(file)){
String errMsg = "#1 Could not rename the file : " + m_fileName;
s_Logger.log(Level.SEVERE, errMsg );
throw new ErrorIdGeneratorException(errMsg);
}
tempStream = new FileOutputStream(file);
FileChannel fileChannel = tempStream.getChannel();
FileLock fileLock = fileChannel.tryLock();
if(fileLock == null){
throw new ErrorIdGeneratorException("#2 Could not get lock for the file : " + m_fileName );
}
} catch ( FileNotFoundException e) {
throw new ErrorIdGeneratorException("Could not make the file writable : " + e.getMessage());
} catch (IOException e) {
throw new ErrorIdGeneratorException("Could not make the file writable " + e.getMessage());
} finally {
CodeGenUtil.closeQuietly(tempStream);
}
return true;
}
private boolean copyFile(File sourceFile, File tempFile) {
InputStream sourceInputStream = null;
OutputStream targetOutputStream = null;
try {
sourceInputStream = new FileInputStream(sourceFile);
targetOutputStream = new FileOutputStream(tempFile);
byte[] bytes = new byte[10000];
int readCount = 0;
while ( (readCount = sourceInputStream.read(bytes)) > 0 ){
targetOutputStream.write(bytes,0,readCount);
}
} catch (FileNotFoundException e) {
s_Logger.log(Level.INFO, e.getMessage());
return false;
} catch (IOException e) {
s_Logger.log(Level.INFO, e.getMessage());
return false;
}
finally{
CodeGenUtil.closeQuietly(targetOutputStream);
CodeGenUtil.closeQuietly(sourceInputStream);
}
return true;
}
private String getLockFilesPath() {
return m_fileName + s_lockFileExtension;
}
/**
*
* @param domain
* @return
*/
private long findAndUpdateNextId( String domain ) {
UsedErrorBlock usedErrorBlock = getUsedErrorBlock( domain );
// TODO: Create a new block
long lastId = usedErrorBlock.getLast() + 1;
usedErrorBlock.setLast( lastId );
return lastId;
}
public void persist() throws ErrorIdGeneratorException {
try {
File file = new File(m_fileName);
m.marshal( errorBlocks, file );
file.setReadOnly();
File lockFile = new File(getLockFilesPath());
if(lockFile.exists()){
if(!lockFile.delete()){
throw new ErrorIdGeneratorException("The lock file could not be deleted");
}
}
} catch ( JAXBException e ) {
throw new ErrorIdGeneratorException( "Failed to save: " + e.getMessage() );
}
}
/*
private void makeTheFileReadOnly() throws ErrorIdGeneratorException{
File file = new File(fileName);
file.setReadOnly();
try {
File file = new File(fileName);
String filePath = file.getAbsolutePath();
Runtime.getRuntime().exec("chmod 111 " + filePath);
} catch (IOException e) {
throw new ErrorIdGeneratorException("Could not make the file read only after updating the file : "+ fileName);
}
}
*/
}
class Range implements Comparable<Range> {
private long start;
private long end;
public long getStart() { return start; }
public long getEnd() { return end; }
public Range( long start, long end ) {
if ( start > end ) throw new IllegalArgumentException();
this.start = start;
this.end = end;
}
public boolean isContained( long n ) {
return start <= n && n < end;
}
public boolean intersects( Range range ) {
return
( this.start < range.start && range.start < this.end ) || // right intersection
( this.start < range.end && range.end < this.end ) || // left intersection
( this.start <= range.start && this.end >= range.end ); // all enveloping
}
public int compareTo( Range rhs ) {
return this.end < rhs.end ? -1 : this.end == rhs.end ? 0 : 1;
}
public String toString() {
return "start="+start + ", end="+end;
}
}