/*
* Data Hub Service (DHuS) - For Space data distribution.
* Copyright (C) 2013,2014,2015 GAEL Systems
*
* This file is part of DHuS software sources.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.gael.dhus.datastore;
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Produce hierarchical directories in depth and limiting occurrences in
* each directories.
*/
public class HierarchicalDirectoryBuilder implements DirectoryBuilder
{
public static final String DHUS_ENTRY_NAME = "dhus_entry";
private static final Logger LOGGER = LogManager.getLogger(HierarchicalDirectoryBuilder.class);
private static Long counter = 0L;
private File root;
private Long maxOccurence;
public HierarchicalDirectoryBuilder (File root, int max_occurence)
{
this.root = root;
this.maxOccurence = new Long(max_occurence);
}
/**
* Build a path from a given counter and according to a maximum number of
* entry per level. The path steps are expressed in hexadecimal numbers with
* an "x" prefix and are separated by a slash. All paths begin by a leading
* slash.
*
* The algorithm is equivalent to the base conversion of the input <code>
* counter</code> to a base radix equal to the <code>max_occurence</code>.
* As a consequence, the <code>counter</code> shall not be negative and the
* <code>max_occurence</code> shall be greater or equal to 2.
*
* Example, for a counter running from 0 to 100 with a max_occurrence of 3:
*
* <pre>
* 0 -> "/x0"
* 1 -> "/x1"
* 2 -> "/x2"
* 3 -> "/x3"
* 4 -> "/x4"
* 5 -> "/x5"
* 6 -> "/x6"
* 7 -> "/x7"
* 8 -> "/x8"
* 9 -> "/x9"
* 10 -> "/xA"
* 11 -> "/xB"
* 12 -> "/xC"
* 13 -> "/xD"
* 14 -> "/xE"
* 15 -> "/xF"
* 16 -> "/x0/x1"
* 17 -> "/x1/x1"
* 18 -> "/x2/x1"
* 19 -> "/x3/x1"
* ...
* 89 -> "/x9/x5"
* 90 -> "/xA/x5"
* 91 -> "/xB/x5"
* 92 -> "/xC/x5"
* 93 -> "/xD/x5"
* 94 -> "/xE/x5"
* 95 -> "/xF/x5"
* 96 -> "/x0/x6"
* 97 -> "/x1/x6"
* 98 -> "/x2/x6"
* 99 -> "/x3/x6"
* </pre>
*
* @param counter the counter to be converted (positive or null).
* @param max_occurrence the maximum number of entries per level (greater or
* equal to 2).
* @return the hierarchical path (never null).
*/
static String getHierarchicalPath (final long counter,
final long max_occurrence) throws IllegalArgumentException
{
// Check that counter is positive or null
if (counter < 0)
{
throw new IllegalArgumentException("Negative counter");
}
// Check that counter is strictly greater than 1
if (max_occurrence <= 1)
{
throw new IllegalArgumentException(
"Maximum occurrence shall be greater than 1");
}
// Prepare output path
String output_path = "";
// Prepare a quotient, equal to the input counter
long running_quotient = counter;
// Loop until quotient reaches 0
do
{
// Concatenate the remainder to the output path
output_path += "/x" +
Long.toHexString(running_quotient % max_occurrence)
.toUpperCase();
// !update the quotient
running_quotient = running_quotient / max_occurrence;
}
while (running_quotient > 0);
// Return the built path
return output_path;
}
/**
* Returns the next available incoming folder.
* @return unused incoming path. This path is available.
*/
@Override
public File getDirectory ()
{
return getUnused ();
}
/**
* @return the root
*/
public File getRoot ()
{
return root;
}
/**
* Reset the path computation counter.
*/
public void resetCounter ()
{
HierarchicalDirectoryBuilder.counter = 0L;
}
/**
* Reset this hierarchical path builder algorithm counter to let the next
* call quickly access to the first free path.
* This method helps to fill empty directories from the hierarchy if any.
*/
void recomputeCounterFirstFreePath ()
{
resetCounter ();
File unused = null;
do
{
String h_path = getHierarchicalPath (counter++, maxOccurence);
unused = new File (new File (getRoot (), h_path), DHUS_ENTRY_NAME);
} while (isUsed (unused));
// place the counter to the fee position.
counter--;
LOGGER.info ("Computed incoming counter to " + counter);
}
private static boolean isHookInstalled = false;
private static String COUNTER_FILE_NAME = ".counter";
/**
* The initialization of incoming consists re-computing the counter.
* For optimization purpose, the counter is saved/retrieved form
* incoming root file. A shutdown hook is installed to save the counter at
* system exit.
*/
void init()
{
// Try to read counter from local file
File root = getRoot();
File counter_file = new File(root, COUNTER_FILE_NAME);
Long counter = null;
if (counter_file.exists ())
{
try
{
String counter_string=FileUtils.readFileToString(counter_file);
counter_string=counter_string.trim();
counter=Long.parseLong(counter_string);
}
catch(IOException e)
{
LOGGER.error("Counter file " + counter_file.getPath() +
" cannot be accessed", e);
}
catch(NumberFormatException nfe)
{
LOGGER.error("Counter file " + counter_file.getPath() +
" malformed: " + nfe.getMessage ());
}
catch(NullPointerException npe)
{
LOGGER.error("Counter file " + counter_file.getPath() +
"is empty.");
}
}
// Counter file found: save it
if(counter != null)
{
HierarchicalDirectoryBuilder.counter = counter;
}
else
{
// Counter file not found: compute it
recomputeCounterFirstFreePath();
}
// Install shutdown hook to save the counter at the end of the
// system execution: shall be run only once.
if(!isHookInstalled)
{
isHookInstalled = true;
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable()
{
@Override
public void run ()
{
try
{
// Recompute the counter path (case of root changed).
File root = getRoot();
File counter_file = new File(root, COUNTER_FILE_NAME);
// Save the counter.
FileUtils.writeStringToFile (counter_file,
HierarchicalDirectoryBuilder.counter.toString ());
}
catch (IOException e)
{
LOGGER.error("Unable to save Incoming counter: " + e.getMessage());
}
}
}));
}
}
/**
* Looks for and creates a new incoming directory according to
* {@link #getHierarchicalPath(Long, Long)} method. If the computed path is
* already used {@link #getUnused()} method recompute it.
* @return available path.
*/
private File getUnused ()
{
File unused = null;
do
{
String h_path = getHierarchicalPath (counter++, maxOccurence);
unused = new File (new File (getRoot (), h_path), DHUS_ENTRY_NAME);
} while (isUsed (unused));
unused.mkdirs ();
return unused;
}
/**
* Checks if the passed path is already used. The path is considered 'used'
* if it has been already created.
* @param path to check.
* @return true if the directory is used, false otherwise.
*/
private boolean isUsed (File path)
{
return path.exists ();
}
// End getHierarchicalPath(long, long)
}