/*
* Copyright (C) 2007 - 2012.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* version 2 as published by the Free Software Foundation.
*
* 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
* General Public License for more details.
*/
package uk.me.parabola.mkgmap.main;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
import java.lang.management.MemoryUsage;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.ArgumentProcessor;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.CommandArgsReader;
import uk.me.parabola.mkgmap.Version;
import uk.me.parabola.mkgmap.combiners.Combiner;
import uk.me.parabola.mkgmap.combiners.FileInfo;
import uk.me.parabola.mkgmap.combiners.GmapiBuilder;
import uk.me.parabola.mkgmap.combiners.GmapsuppBuilder;
import uk.me.parabola.mkgmap.combiners.MdrBuilder;
import uk.me.parabola.mkgmap.combiners.MdxBuilder;
import uk.me.parabola.mkgmap.combiners.NsisBuilder;
import uk.me.parabola.mkgmap.combiners.OverviewBuilder;
import uk.me.parabola.mkgmap.combiners.TdbBuilder;
import uk.me.parabola.mkgmap.osmstyle.StyleFileLoader;
import uk.me.parabola.mkgmap.osmstyle.StyleImpl;
import uk.me.parabola.mkgmap.reader.osm.Style;
import uk.me.parabola.mkgmap.reader.osm.StyleInfo;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.srt.SrtTextReader;
import uk.me.parabola.util.EnhancedProperties;
/**
* The new main program. There can be many file names to process and there can
* be differing outputs determined by options. So the actual work is mostly
* done in other classes. This one just works out what is wanted.
*
* @author Steve Ratcliffe
*/
public class Main
implements ArgumentProcessor
{
private static final Logger log =
Logger.
getLogger(Main.
class);
private static final Date StartTime =
new Date();
// Final .img file combiners.
private final List<Combiner
> combiners =
new ArrayList<>();
private final Map<String, MapProcessor
> processMap =
new HashMap<>();
private String styleFile =
"classpath:styles";
private String styleOption
;
private boolean verbose
;
private final List<FilenameTask
> futures =
new LinkedList<>();
private ExecutorService threadPool
;
// default number of threads
private int maxJobs =
0;
private boolean createTdbFiles =
false;
private boolean tdbBuilderAdded =
false;
// used for messages in listStyles and checkStyles
private String searchedStyleName
;
private volatile int programRC =
0;
private final Map<String, Combiner
> combinerMap =
new HashMap<>();
private final Map<String,
String> sourceMap =
new HashMap<>();
private boolean informationDisplayed =
false;
/**
* Used for unit tests
*/
public static void mainNoSystemExit
(String...
args) {
Main.
mainStart(args
);
}
public static void main
(String...
args) {
int rc = Main.
mainStart(args
);
if (rc
!=
0)
System.
exit(1);
}
/**
* The main program to make or combine maps. We now use a two pass process,
* first going through the arguments and make any maps and collect names
* to be used for creating summary files like the TDB and gmapsupp.
*
* @param args The command line arguments.
*/
private static int mainStart
(String...
args) {
Instant start = Instant.
now();
// We need at least one argument.
if (args.
length < 1) {
printUsage
();
printHelp
(System.
err, getLang
(),
"options");
return 0;
}
Main mm =
new Main
();
int numExitExceptions =
0;
CommandArgsReader commandArgs =
new CommandArgsReader
(mm
);
try {
// Read the command line arguments and process each filename found.
commandArgs.
setValidOptions(getValidOptions
(System.
err));
commandArgs.
readArgs(args
);
} catch (OutOfMemoryError e
) {
++numExitExceptions
;
String message =
"Out of memory.\r\n";
if (mm.
maxJobs > 1)
message +=
"Try using the mkgmap --max-jobs option with a value less than " + mm.
maxJobs +
" to reduce the memory requirement, or use the Java -Xmx option to increase the available heap memory.";
else
message +=
"Try using the Java -Xmx option to increase the available heap memory.";
Logger.
defaultLogger.
error(message, e
);
} catch (MapFailedException | ExitException e
) {
// one of the combiners failed
++numExitExceptions
;
String message = e.
getMessage();
Throwable cause = e.
getCause();
while (cause
!=
null) {
message +=
"\r\n" + cause.
toString();
cause = cause.
getCause();
}
Logger.
defaultLogger.
error(message
);
}
if(commandArgs.
getHasFiles()) {
Logger.
defaultLogger.
write("Number of ExitExceptions: " + numExitExceptions
);
Logger.
defaultLogger.
write("Time finished: " +
new Date());
Duration duration =
Duration.
between(start, Instant.
now());
long seconds = duration.
getSeconds();
if (seconds
> 0) {
long hours = seconds /
3600;
seconds -= hours
* 3600;
long minutes = seconds /
60;
seconds -= minutes
* 60;
Logger.
defaultLogger.
write("Total time taken: " +
(hours
> 0 ? hours +
(hours
> 1 ? " hours " :
" hour ") :
"") +
(minutes
> 0 ? minutes +
(minutes
> 1 ? " minutes " :
" minute ") :
"") +
(seconds
> 0 ? seconds +
(seconds
> 1 ? " seconds" :
" second") :
""));
}
else
Logger.
defaultLogger.
write("Total time taken: " + duration.
getNano() /
1000000 +
" ms");
}
else if (numExitExceptions ==
0 && !mm.
informationDisplayed)
System.
err.
println("The command line does not appear to require mkgmap to do anything.");
return (numExitExceptions
> 0 || mm.
getProgramRC() !=
0) ? 1 :
0;
}
private static void printUsage
(){
System.
err.
println("Usage: mkgmap [options...] <file.osm>");
}
private void setProgramRC
(int rc
){
programRC = rc
;
}
private int getProgramRC
(){
return programRC
;
}
/**
* Grab the options help file and print it.
* @param err The output print stream to write to.
* @param lang A language hint. The help will be displayed in this
* language if it has been translated.
* @param file The help file to display.
*/
private static void printHelp
(PrintStream err,
String lang,
String file
) {
String path =
"/help/" + lang +
'/' + file
;
try (InputStream stream = Main.
class.
getResourceAsStream(path
)) {
if (stream ==
null) {
err.
println("Could not find the help topic: " + file +
", sorry");
return;
}
BufferedReader r =
new BufferedReader(new InputStreamReader(stream, StandardCharsets.
UTF_8));
String line
;
while ((line = r.
readLine()) !=
null)
err.
println(line
);
} catch (IOException e
) {
err.
println("Could not read the help topic: " + file +
", sorry");
}
}
private static Set<String> getValidOptions
(PrintStream err
) {
try (InputStream stream = Main.
class.
getResourceAsStream("/help/en/options")) {
if (stream ==
null)
return Collections.
emptySet();
Set<String> knownOptions =
new HashSet<>();
BufferedReader r =
new BufferedReader(new InputStreamReader(stream, StandardCharsets.
UTF_8));
Pattern p =
Pattern.
compile("^--?([a-zA-Z0-9-]*).*$");
String line
;
while ((line = r.
readLine()) !=
null) {
Matcher matcher = p.
matcher(line
);
if (matcher.
matches()) {
knownOptions.
add(matcher.
group(1));
}
}
return knownOptions
;
} catch (IOException e
) {
err.
println("Could not read valid options");
return Collections.
emptySet();
}
}
public void startOptions
() {
/**
* A null implementation that just returns the input name as the output.
*/
MapProcessor saver =
(args, filename
) -
> filename
;
processMap.
put("img", saver
);
processMap.
put("mdx", saver
);
processMap.
put("typ",
new TypSaver
());
// Normal map files.
processMap.
put("rgn", saver
);
processMap.
put("tre", saver
);
processMap.
put("lbl", saver
);
processMap.
put("net", saver
);
processMap.
put("nod", saver
);
processMap.
put("txt",
new TypCompiler
());
}
/**
* Switch out to the appropriate class to process the filename.
*/
public void processFilename
(final CommandArgs args,
final String filename
) {
final String ext = extractExtension
(filename
);
log.
debug("file", filename,
", extension is", ext
);
// ignore ovm_* files given as command line arguments
if (OverviewBuilder.
isOverviewImg(filename
))
return;
final MapProcessor mp = mapMaker
(ext
);
args.
setSort(getSort
(args
));
log.
info("Submitting job " + filename
);
FilenameTask task =
new FilenameTask
(() -
> {
log.
threadTag(filename
);
if (filename.
startsWith("test-map:") ||
new File(filename
).
exists()){
String output = mp.
makeMap(args, filename
);
log.
debug("adding output name", output
);
log.
threadTag(null);
return output
;
} else {
log.
error("input file '" + filename +
"' doesn't exist");
return null;
}
});
task.
setArgs(args
);
task.
setSource(filename
);
futures.
add(task
);
}
private MapProcessor mapMaker
(String ext
) {
MapProcessor mp = processMap.
get(ext
);
if (mp ==
null)
mp =
new MapMaker
(createTdbFiles
);
return mp
;
}
public void processOption
(String opt,
String val
) {
log.
debug("option:", opt, val
);
switch (opt
) {
case "number-of-files":
// This option always appears first. We use it to turn on/off
// generation of the overview files if there is only one file
// to process.
int n =
Integer.
parseInt(val
);
if (n
> 0) // TODO temporary, this option will become properly default of on.
createTdbFiles =
true;
break;
case "help":
informationDisplayed =
true;
printHelp
(System.
out, getLang
(),
(!val.
isEmpty()) ? val :
"help");
break;
case "style-file":
case "map-features":
styleFile = val
;
break;
case "style":
styleOption = val
;
break;
case "verbose":
verbose =
true;
break;
case "list-styles":
informationDisplayed =
true;
listStyles
();
break;
case "check-styles":
informationDisplayed =
true;
checkStyles
();
break;
case "max-jobs":
if (val.
isEmpty())
maxJobs =
Runtime.
getRuntime().
availableProcessors();
else {
maxJobs =
Integer.
parseInt(val
);
if (maxJobs
< 1) {
Logger.
defaultLogger.
warn("max-jobs has to be at least 1");
maxJobs =
1;
}
if (maxJobs
> Runtime.
getRuntime().
availableProcessors())
Logger.
defaultLogger.
warn("It is recommended that max-jobs be no greater that the number of processor cores");
}
break;
case "version":
informationDisplayed =
true;
System.
err.
println("Mkgmap version " + Version.
VERSION);
System.
err.
println(Version.
VERSION);
System.
exit(0);
}
}
public void removeOption
(String opt
) {
if ("tdbfile".
equals(opt
))
createTdbFiles =
false;
}
/**
* Add the builders for the TDB and overview map. These are always
* generated together as we use some info that is calculated when constructing
* the overview map in the TDB file.
*/
private void addTdbBuilder
() {
if (!tdbBuilderAdded
){
OverviewBuilder overviewBuilder =
new OverviewBuilder
();
addCombiner
("img", overviewBuilder
);
TdbBuilder tdbBuilder =
new TdbBuilder
(overviewBuilder
);
addCombiner
("tdb", tdbBuilder
);
tdbBuilderAdded =
true;
}
}
private void listStyles
() {
String[] names
;
try {
StyleFileLoader loader = StyleFileLoader.
createStyleLoader(styleFile,
null);
names = loader.
list();
loader.
close();
} catch (FileNotFoundException e
) {
log.
debug("didn't find style file", e
);
throw new ExitException
("Could not list style file " + styleFile
);
}
Arrays.
sort(names
);
System.
out.
println("The following styles are available:");
for (String name : names
) {
Style style = readOneStyle
(name,
false);
if (style ==
null)
continue;
StyleInfo info = style.
getInfo();
System.
out.
format("%-15s %6s: %s\n",
searchedStyleName,info.
getVersion(), info.
getSummary());
if (verbose
) {
for (String s : info.
getLongDescription().
split("\n"))
System.
out.
printf("\t%s\n", s.
trim());
}
}
}
/**
* Check one or all styles in the path given in styleFile.
*/
private void checkStyles
() {
String[] names
;
try {
StyleFileLoader loader = StyleFileLoader.
createStyleLoader(styleFile,
null);
names = loader.
list();
loader.
close();
} catch (FileNotFoundException e
) {
log.
debug("didn't find style file", e
);
throw new ExitException
("Could not check style file " + styleFile
);
}
Arrays.
sort(names
);
if (styleOption ==
null){
if (names.
length > 1)
System.
out.
println("The following styles are available:");
else
System.
out.
println("Found one style in " + styleFile
);
}
int checked =
0;
for (String name : names
) {
if (styleOption
!=
null && !Objects.
equals(name, styleOption
))
continue;
if (names.
length > 1){
System.
out.
println("checking style: " + name
);
}
++checked
;
boolean performChecks =
true;
if ("classpath:styles".
equals(styleFile
) && !"default".
equals(name
)) {
performChecks =
false;
}
Style style = readOneStyle
(name, performChecks
);
if (style ==
null){
System.
out.
println("could not open style " + name
);
}
}
if (checked ==
0)
System.
out.
println("could not open style " + styleOption +
" in " + styleFile
);
System.
out.
println("finished check-styles");
}
/**
* Try to read a style from styleFile directory
* @param name name of the style
* @param performChecks perform checks?
* @return the style or null in case of errors
*/
private Style readOneStyle
(String name,
boolean performChecks
){
searchedStyleName = name
;
Style style =
null;
try {
style =
new StyleImpl
(styleFile, name,
new EnhancedProperties
(), performChecks
);
} catch (SyntaxException e
) {
Logger.
defaultLogger.
error("Error in style: " + e.
getMessage());
} catch (FileNotFoundException e
) {
log.
debug("could not find style", name
);
try {
searchedStyleName =
new File(styleFile
).
getName();
style =
new StyleImpl
(styleFile,
null,
new EnhancedProperties
(), performChecks
);
} catch (SyntaxException e1
) {
Logger.
defaultLogger.
error("Error in style: " + e1.
getMessage());
} catch (FileNotFoundException e1
) {
log.
debug("could not find style", styleFile
);
}
}
return style
;
}
private static String getLang
() {
return "en";
}
private void addCombiner
(String name, Combiner combiner
) {
combinerMap.
put(name, combiner
);
combiners.
add(combiner
);
}
public void endOptions
(CommandArgs args
) {
fileOptions
(args
);
int taskCount = futures.
size();
if (taskCount
> 0) {
Logger.
defaultLogger.
write("Mkgmap version " + Version.
VERSION);
Logger.
defaultLogger.
write("Time started: " + StartTime
);
}
log.
info("Start tile processors");
int threadCount = maxJobs
;
Runtime runtime =
Runtime.
getRuntime();
if (threadPool ==
null) {
if (threadCount ==
0) {
threadCount =
1;
if (taskCount
> 2) {
//run one task to see how much memory it uses
log.
info("Max Memory: " + runtime.
maxMemory());
futures.
get(0).
run();
long maxMemory =
0;
for (MemoryPoolMXBean mxBean :
ManagementFactory.
getMemoryPoolMXBeans())
{
if (mxBean.
getType() ==
MemoryType.
HEAP) {
MemoryUsage memoryUsage = mxBean.
getPeakUsage();
log.
info("Max: " + memoryUsage.
getMax());
log.
info("Used: " + memoryUsage.
getUsed());
if (memoryUsage.
getMax() > maxMemory
&& memoryUsage.
getUsed() !=
0) {
maxMemory = memoryUsage.
getMax();
threadCount =
(int)(memoryUsage.
getMax() / memoryUsage.
getUsed());
}
}
}
threadCount =
Math.
max(threadCount,
1);
threadCount =
Math.
min(threadCount, runtime.
availableProcessors());
Logger.
defaultLogger.
warn("Setting max-jobs to " + threadCount
);
}
}
log.
info("Creating thread pool with " + threadCount +
" threads");
threadPool =
Executors.
newFixedThreadPool(threadCount
);
}
// process all input files
for (FilenameTask task : futures
) {
threadPool.
execute(task
);
}
List<FilenameTask
> filenames =
new ArrayList<>();
int numMapFailedExceptions =
0;
if (threadPool
!=
null) {
threadPool.
shutdown();
while (!futures.
isEmpty()) {
try {
try {
// don't call get() until a job has finished
if (futures.
get(0).
isDone()) {
FilenameTask future = futures.
remove(0);
// Provoke any exceptions by calling get and then
// save the result for later use
future.
setFilename(future.
get());
filenames.
add(future
);
} else {
Thread.
sleep(100);
}
} catch (ExecutionException e
) {
// Re throw the underlying exception
Throwable cause = e.
getCause();
if (cause
instanceof Exception)
//noinspection ProhibitedExceptionThrown
throw (Exception) cause
;
else if (cause
instanceof Error)
//noinspection ProhibitedExceptionThrown
throw (Error) cause
;
else
throw e
;
}
} catch (OutOfMemoryError | ExitException e
) {
throw e
;
} catch (MapFailedException mfe
) {
numMapFailedExceptions++
;
setProgramRC
(-
1);
if (!args.
getProperties().
getProperty("keep-going",
false)) {
throw new ExitException
("Exiting - if you want to carry on regardless, use the --keep-going option");
}
} catch (Exception e
) {
Logger.
defaultLogger.
error("Unexpected error", e
);
throw new ExitException
("Exiting due to unexpected error");
}
}
}
Logger.
defaultLogger.
write("Number of MapFailedExceptions: " + numMapFailedExceptions
);
if ((taskCount
> threadCount +
1) && (maxJobs ==
0) && (threadCount
< runtime.
availableProcessors())) {
Logger.
defaultLogger.
warn("To reduce the run time, consider increasing the amnount of memory available for use by mkgmap by using the Java -Xmx flag to set the memory to more than " +
100* (1 +
((runtime.
maxMemory() * runtime.
availableProcessors()) /
(threadCount
* 1024 * 1024 * 100))) +
" MB, providing this is less than the amount of physical memory installed.");
}
if (combiners.
isEmpty())
return;
boolean hasFiles =
false;
for (FilenameTask file : filenames
) {
if (file ==
null || file.
isCancelled() || file.
getFilename() ==
null){
if (args.
getProperties().
getProperty("keep-going",
false))
continue;
else
throw new ExitException
("Exiting - if you want to carry on regardless, use the --keep-going option");
}
hasFiles =
true;
}
if (!hasFiles
){
log.
info("nothing to do for combiners.");
return;
}
log.
info("Combining maps");
args.
setSort(getSort
(args
));
// Get them all set up.
for (Combiner c : combiners
)
c.
init(args
);
filenames.
removeIf(f -
> f ==
null || f.
getFilename() ==
null || f.
isCancelled());
final Map<String,
Integer> nameToHex =
new HashMap<>();
for (FilenameTask f : filenames
) {
if (f.
getFilename().
endsWith(".img")) {
sourceMap.
put(f.
getFilename(), f.
getSource());
int hex
;
try {
hex = FileInfo.
getFileInfo(f.
getFilename() ).
getHexname();
} catch (FileNotFoundException ignored
) {
hex =
0;
}
nameToHex.
put(f.
getFilename(), hex
);
}
}
filenames.
sort((o1, o2
) -
> {
if (!o1.
getFilename().
endsWith(".img") ||
!o2.
getFilename().
endsWith(".img"))
return o1.
getFilename().
compareTo(o2.
getFilename());
// Both end in .img
return Integer.
compare(nameToHex.
getOrDefault(o1.
getFilename(),
0), nameToHex.
getOrDefault(o2.
getFilename(),
0));
});
// will contain img files for which an additional ovm file was found
HashSet<String> foundOvmFiles =
new HashSet<>();
// try OverviewBuilder with special files
if (tdbBuilderAdded
){
for (FilenameTask file : filenames
) {
try {
String fileName = file.
getFilename();
if (!fileName.
endsWith(".img"))
continue;
File f1 =
new File(fileName
);
fileName =
new File(f1.
getParent(), OverviewBuilder.
getOverviewImgName(fileName
)).
getAbsolutePath();
log.
info(" " + fileName
);
FileInfo fileInfo = FileInfo.
getFileInfo(fileName
);
fileInfo.
setArgs(file.
getArgs());
// add the real input file
foundOvmFiles.
add(file.
getFilename());
for (Combiner c : combiners
){
if (c
instanceof OverviewBuilder
)
c.
onMapEnd(fileInfo
);
}
} catch (FileNotFoundException ignored
) {
}
}
}
// Tell them about each filename (OverviewBuilder excluded)
for (FilenameTask file : filenames
) {
try {
log.
info(" " + file
);
FileInfo fileInfo = FileInfo.
getFileInfo(file.
getFilename());
fileInfo.
setArgs(file.
getArgs());
for (Combiner c : combiners
){
if (c
instanceof OverviewBuilder
&& foundOvmFiles.
contains(file.
getFilename()))
continue;
c.
onMapEnd(fileInfo
);
}
fileInfo.
closeMapReader();
} catch (FileNotFoundException e
) {
throw new MapFailedException
("could not open file " + e.
getMessage());
}
}
// All done, allow tidy up or file creation to happen
for (Combiner c : combiners
)
c.
onFinish();
if (tdbBuilderAdded
&& args.
getProperties().
getProperty("remove-ovm-work-files",
false)){
for (String fName : foundOvmFiles
) {
String ovmFile = OverviewBuilder.
getOverviewImgName(fName
);
File f =
new File(args.
getOutputDir(), ovmFile
);
if (f.
exists() && f.
isFile()) {
try {
Files.
delete(f.
toPath());
log.
info("removed " + f
);
} catch (IOException e
) {
log.
warn("removing " + f +
"failed with " + e.
getMessage());
}
}
}
}
}
private void fileOptions
(CommandArgs args
) {
boolean indexOpt = args.
exists("index");
boolean gmapsuppOpt = args.
exists("gmapsupp");
boolean tdbOpt = args.
exists("tdbfile");
boolean gmapiOpt = args.
exists("gmapi") || args.
exists("gmapi-minimal");
boolean nsisOpt = args.
exists("nsis");
if (args.
exists("gmapi") && args.
exists("gmapi-minimal")) {
throw new ExitException
("Options --gmapi and --gmapi-minimal are mutually exclusive");
}
for (String opt :
Arrays.
asList("gmapi",
"nsis",
"gmapi-minimal")) {
if (!createTdbFiles
&& args.
exists(opt
)) {
throw new ExitException
("Options --" + opt +
" and --no-tdbfiles are mutually exclusive");
}
}
if (tdbOpt || createTdbFiles
){
addTdbBuilder
();
}
if (gmapsuppOpt
) {
GmapsuppBuilder gmapBuilder =
new GmapsuppBuilder
();
gmapBuilder.
setCreateIndex(indexOpt
);
addCombiner
("gmapsupp", gmapBuilder
);
}
if (indexOpt
&& (tdbOpt ||
!gmapsuppOpt || gmapiOpt || nsisOpt
)) {
addCombiner
("mdr",
new MdrBuilder
());
addCombiner
("mdx",
new MdxBuilder
());
}
if (gmapiOpt
) {
addCombiner
("gmapi",
new GmapiBuilder
(combinerMap, sourceMap
));
}
if (nsisOpt
) {
addCombiner
("nsis",
new NsisBuilder
(combinerMap
));
}
}
/**
* Get the extension of the filename, ignoring any compression suffix.
*
* @param filename The original filename.
* @return The file extension.
*/
private static String extractExtension
(String filename
) {
String[] parts = filename.
toLowerCase(Locale.
ENGLISH).
split("\\.");
List<String> ignore =
Arrays.
asList("gz",
"bz2",
"bz");
// We want the last part that is not gz, bz etc (and isn't the first part ;)
for (int i = parts.
length -
1; i
> 0; i--
) {
String ext = parts
[i
];
if (!ignore.
contains(ext
))
return ext
;
}
return "";
}
/**
* Create the sort description for the map. This is used to sort items in the files
* and also is converted into a SRT file which is included in the MDR file.
*
* We simply use the code page to locate a sorting description, but it would be possible to
* specify the sort separately.
*
* @return A sort description object.
*/
public Sort getSort
(CommandArgs args
) {
return SrtTextReader.
sortForCodepage(args.
getCodePage());
}
private static class FilenameTask
extends FutureTask<String> {
private CommandArgs args
;
private String filename
;
private String source
;
private FilenameTask
(Callable<String> callable
) {
super(callable
);
}
public void setArgs
(CommandArgs args
) {
this.
args = args
;
}
public CommandArgs getArgs
() {
return args
;
}
public void setFilename
(String filename
) {
this.
filename = filename
;
}
public String getFilename
() {
return filename
;
}
public String toString
() {
return source +
" -> " + filename
;
}
public void setSource
(String source
) {
this.
source = source
;
}
public String getSource
() {
return source
;
}
}
}