001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.transaction.file;
018
019import java.io.BufferedReader;
020import java.io.BufferedWriter;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.io.OutputStream;
029import java.io.OutputStreamWriter;
030import java.io.UnsupportedEncodingException;
031
032import org.apache.commons.transaction.util.FileHelper;
033import org.apache.commons.transaction.util.LoggerFacade;
034
035/**
036 * Fail-Safe sequence store implementation using the file system. Works by versioning
037 * values of sequences and throwing away all versions, but the current and the previous one.
038 * 
039 * @version $Id: FileSequence.java 493628 2007-01-07 01:42:48Z joerg $
040 */
041public class FileSequence {
042
043    protected final String storeDir;
044    protected final LoggerFacade logger;
045
046    /**
047     * Creates a new resouce manager operation on the specified directories.
048     * 
049     * @param storeDir directory where sequence information is stored
050     * @param logger logger used for warnings only
051     */
052    public FileSequence(String storeDir, LoggerFacade logger) throws ResourceManagerException {
053        this.storeDir = storeDir;
054        this.logger = logger;
055        File file = new File(storeDir);
056        file.mkdirs();
057        if (!file.exists()) {
058            throw new ResourceManagerException("Can not create working directory " + storeDir);
059        }
060    }
061
062        /**
063         * Checks if the sequence already exists.
064         * 
065         * @param sequenceName the name of the sequence you want to check 
066         * @return <code>true</code> if the sequence already exists, <code>false</code> otherwise
067         */
068    public synchronized boolean exists(String sequenceName) {
069        String pathI = getPathI(sequenceName);
070        String pathII = getPathII(sequenceName);
071
072        return (FileHelper.fileExists(pathI) || FileHelper.fileExists(pathII));
073    }
074
075        /**
076         * Creates a sequence if it does not already exist.
077         * 
078         * @param sequenceName the name of the sequence you want to create 
079         * @return <code>true</code> if the sequence has been created, <code>false</code> if it already existed
080         * @throws ResourceManagerException if anything goes wrong while accessing the sequence 
081         */
082    public synchronized boolean create(String sequenceName, long initialValue) throws ResourceManagerException {
083        if (exists(sequenceName))
084            return false;
085        write(sequenceName, initialValue);
086        return true;
087    }
088
089        /**
090         * Deletes a sequence if it exists.
091         * 
092         * @param sequenceName the name of the sequence you want to delete 
093         * @return <code>true</code> if the sequence has been deleted, <code>false</code> if not
094         */
095    public synchronized boolean delete(String sequenceName) {
096        if (!exists(sequenceName))
097            return false;
098        String pathI = getPathI(sequenceName);
099        String pathII = getPathII(sequenceName);
100
101        // XXX be careful no to use shortcut eval with || might not delete second file        
102        boolean res1 = FileHelper.deleteFile(pathI);
103        boolean res2 = FileHelper.deleteFile(pathII);
104
105        return (res1 || res2);
106    }
107
108        /**
109         * Gets the next value of the sequence. 
110         * 
111         * @param sequenceName the name of the sequence you want the next value for
112         * @param increment the increment for the sequence, i.e. how much to add to the sequence with this call
113         * @return the next value of the sequence <em>not yet incremented</em>, i.e. the increment is recorded
114         * internally, but not returned with the next call to this method
115         * @throws ResourceManagerException if anything goes wrong while accessing the sequence 
116         */
117    public synchronized long nextSequenceValueBottom(String sequenceName, long increment)
118        throws ResourceManagerException {
119        if (!exists(sequenceName)) {
120            throw new ResourceManagerException("Sequence " + sequenceName + " does not exist");
121        }
122        if (increment <= 0) {
123            throw new IllegalArgumentException("Increment must be greater than 0, was " + increment);
124        }
125        long value = read(sequenceName);
126        long newValue = value + increment;
127        write(sequenceName, newValue);
128        return value;
129    }
130
131    protected long read(String sequenceName) throws ResourceManagerException {
132        String pathI = getPathI(sequenceName);
133        String pathII = getPathII(sequenceName);
134
135        long returnValue = -1;
136
137        long valueI = -1;
138        if (FileHelper.fileExists(pathI)) {
139            try {
140                valueI = readFromPath(pathI);
141            } catch (NumberFormatException e) {
142                throw new ResourceManagerException("Fatal internal error: Backup sequence value corrupted");
143            } catch (FileNotFoundException e) {
144                throw new ResourceManagerException("Fatal internal error: Backup sequence vanished");
145            } catch (IOException e) {
146                throw new ResourceManagerException("Fatal internal error: Backup sequence value corrupted");
147            }
148        }
149
150        long valueII = -1;
151        if (FileHelper.fileExists(pathII)) {
152            try {
153                valueII = readFromPath(pathII);
154                if (valueII > valueI) {
155                    returnValue = valueII;
156                } else {
157                    // if it is smaller than previous this *must* be an error as we constantly increment
158                    logger.logWarning("Latest sequence value smaller than previous, reverting to previous");
159                    FileHelper.deleteFile(pathII);
160                    returnValue = valueI;
161                }
162            } catch (NumberFormatException e) {
163                logger.logWarning("Latest sequence value corrupted, reverting to previous");
164                FileHelper.deleteFile(pathII);
165                returnValue = valueI;
166            } catch (FileNotFoundException e) {
167                logger.logWarning("Can not find latest sequence value, reverting to previous");
168                FileHelper.deleteFile(pathII);
169                returnValue = valueI;
170            } catch (IOException e) {
171                logger.logWarning("Can not read latest sequence value, reverting to previous");
172                FileHelper.deleteFile(pathII);
173                returnValue = valueI;
174            }
175        } else {
176            logger.logWarning("Can not read latest sequence value, reverting to previous");
177            returnValue = valueI;
178        }
179
180        if (returnValue != -1) {
181            return returnValue;
182        } else {
183            throw new ResourceManagerException("Fatal internal error: Could not compute valid sequence value");
184        }
185    }
186
187    protected void write(String sequenceName, long value) throws ResourceManagerException {
188        String pathII = getPathII(sequenceName);
189
190        File f2 = new File(pathII);
191        // by contract when this method is called an f2 exists it must be valid
192        if (f2.exists()) {
193            // move previous value to backup position
194            String pathI = getPathI(sequenceName);
195            File f1 = new File(pathI);
196            f1.delete();
197            if (!f2.renameTo(f1)) {
198                throw new ResourceManagerException("Fatal internal error: Can not create backup value at" + pathI);
199            }
200        }
201        try {
202            if (!f2.createNewFile()) {
203                throw new ResourceManagerException("Fatal internal error: Can not create new value at" + pathII);
204            }
205        } catch (IOException e) {
206            throw new ResourceManagerException("Fatal internal error: Can not create new value at" + pathII, e);
207        }
208        writeToPath(pathII, value);
209    }
210
211    protected String getPathI(String sequenceName) {
212        return storeDir + "/" + sequenceName + "_1.seq";
213    }
214
215    protected String getPathII(String sequenceName) {
216        return storeDir + "/" + sequenceName + "_2.seq";
217    }
218
219    protected long readFromPath(String path)
220        throws ResourceManagerException, NumberFormatException, FileNotFoundException, IOException {
221        File file = new File(path);
222        BufferedReader reader = null;
223        try {
224            InputStream is = new FileInputStream(file);
225
226            // we do not care for encoding as we only have numbers
227            reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
228            String valueString = reader.readLine();
229            long value = Long.parseLong(valueString);
230            return value;
231        } catch (UnsupportedEncodingException e) {
232            throw new ResourceManagerException("Fatal internal error, encoding UTF-8 unknown");
233        } finally {
234            if (reader != null) {
235                try {
236                    reader.close();
237                } catch (IOException e) {
238                }
239
240            }
241        }
242    }
243
244    protected void writeToPath(String path, long value) throws ResourceManagerException {
245        File file = new File(path);
246        BufferedWriter writer = null;
247        try {
248            OutputStream os = new FileOutputStream(file);
249            writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
250            String valueString = Long.toString(value);
251            writer.write(valueString);
252            writer.write('\n');
253        } catch (FileNotFoundException e) {
254            throw new ResourceManagerException("Fatal internal error: Can not find sequence at " + path);
255        } catch (IOException e) {
256            throw new ResourceManagerException("Fatal internal error: Can not write to sequence at " + path);
257        } finally {
258            if (writer != null) {
259                try {
260                    writer.close();
261                } catch (IOException e) {
262                }
263
264            }
265        }
266    }
267}