I ran across an issue when attempting to port an application over to JApplet so it can run on a browser.
Program Contents:
Jar file. Contains my CustomClassLoader implementation. Stored on website.
Content directory. Filled with compiled classes. Stored on the users computer.
Issue:
I am getting a NoClassDefFoundError when attempting to load .class files in the content directory with my CustomClassLoader.
The error, although unattainable, relates back to a class inside the jar. The class is abstract. All the .class files in the content directory extend this class and fill all the required methods. Upon loading these classes, the error is thrown. The program, when ran normally java -jar file.jar, works perfectly fine.
This makes me believe it has to do with the classpath.
Security Setup:
I am running the applet through the appletviewer command like so:
appletviewer -J-Djava.security.policy=policy file.html
In the same directory is my policy file:
grant {
permission java.lang.RuntimePermission "getenv.APPDATA";
permission java.io.FilePermission "<<ALL FILES>>", "read, write, delete, execute";
permission java.lang.RuntimePermission "exitVM";
permission java.util.PropertyPermission "user.name", "read";
permission java.lang.RuntimePermission "createClassLoader";
};
As far as I know, no other security exceptions are being thrown. The applet is signed.
HTML File Used To Load Applet:
<!DOCTYPE html>
<html>
<body>
<object width="1000" height="600" classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
codebase="http://java.sun.com/products/plugin/autodl/jinstall-1_4-windows-i586.cab#Version=1,4,0,0">
<param name="archive" value="file.jar"/>
<param name="code" value="package.to.Boot"/>
</object>
</body>
</html>
Any help towards fixing this problem is greatly appreciated.
CustomClassLoader.java:
package org.obicere.cc.methods;
import java.io.File;
public class CustomClassLoader extends ClassLoader {
//...
private Class<?> loadClass(final File file) {
try {
final byte[] data = IOUtils.readData(file);
return super.defineClass(file.getName().substring(0, file.getName().length() - 6), data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Example Runner: CanReachRunner.java
import java.lang.reflect.Method;
import java.util.Random;
import org.obicere.cc.executor.Result;
import org.obicere.cc.tasks.projects.Runner;
public class CanReachRunner extends Runner {
#Override
public Result[] getResults(Class<?> clazz) {
try {
final Method method = clazz.getMethod("canReach", int.class, int.class, int.class);
final Random ran = new Random();
final Result[] results = new Result[10];
for (int i = 0; i < 10; i++) {
final int small = ran.nextInt(5) + 5;
final int large = ran.nextInt(5);
final int goal = (small + large * 5) + 5 + ran.nextInt(6);
results[i] = new Result(method.invoke(clazz.newInstance(), small, large, goal), (goal <= small + large * 5) && goal % 5 <= small, small, large, goal);
}
return results;
} catch (Exception e) {
return new Result[] {};
}
}
}
There are several things wrong with the class loader. The first is that the loadClass method uses an argument of a String rather than a File, the string being the name of the class to load. This is because the class to load might not be in a file, it might be on a network connection, and anyway the JVM doesn't know how to find the file. The second is that it is bad practice to override loadClass, because if you do, it interferes with the default behavior, which first tries to load classes the normal way, and only resorts to calling the findClass method if that doesn't work. So, you should override findClass instead of defineClass. Here's the updated code:
public class CustomClassLoader extends ClassLoader {
private Class<?> findClass(String class) {
try {
File contentDir = ...; // You have to fill this in with the location of the content dir
final byte[] data = IOUtils.readData(new File(contentDir, class + ".class");
return defineClass(class, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
You must find the content directory somehow and use that to initialize contentDir.
The reason this works when run as a jar is cause it is then capable of loading the classes without needing a custom class loader.
Related
I've come across many posts about these two topics: Auto-Updating and URLClassloaders. I'll start with the auto updating goal. I found this post here that talks about a 2 jar system. One jar that launches the main app jar: From Stephen C:
The launcher could be a Java application that creates a classloader for the new JAR, loads an entrypoint class and calls some method on it. If you do it this way, you have to watch for classloader storage leaks, but that's not difficult. (You just need to make sure that no objects with classes loaded from the JAR are reachable after you relaunch.)
This is the approach I'm taking, but I'm open to other ideas if they prove easier and/or more reliable. The Coordinator has posted some pretty cool launcher code to which I plan on incorporating some of this reload type code in my launcher, but first I need to get it to work.
My issue is that my main app jar has many other dependencies, and I cannot get some of those classes to load despite the fact that all the jars have been added to the URL's array. This brings up the second topic URLClassloader.
Side Note for future readers: When passing a URL to the URLClassloader that is a directory, a helpful note that would have saved me (an embarrassingly large) amount of time is that the contents of the directory must be .class files! I was originally pointing to my dependent jar directory, no good.
Context for the code below, my launcher jar resides in the same directory as my app jar, which is why I'm using user.dir. I will probably change this, but for now the code works and gets far enough into my app's code to request a connection to a sqlite database before failing.
Launcher:
public class Launcher {
public static void main(String[] args) {
try {
String userdir = System.getProperty("user.dir");
File parentDir = new File(userdir);
ArrayList<URL> urls = getJarURLs(parentDir);
URL[] jarURLs = new URL[urls.size()];
int index = 0;
for (URL u : urls) {
System.out.println(u.toString());
jarURLs[index] = u;
index ++;
}
URLClassLoader urlCL = new URLClassLoader(jarURLs);
Class<?> c = urlCL.loadClass("main.AppStart");
Object [] args2 = new Object[] {new String[] {}};
c.getMethod("main", String[].class).invoke(null, args2);
urlCL.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
public static ArrayList<URL> getJarURLs(File parentDir) throws MalformedURLException {
ArrayList<URL> list = new ArrayList<>();
for (File f : parentDir.listFiles()) {
if (f.isDirectory()) {
list.addAll(getJarURLs(f));
} else {
String name = f.getName();
if (name.endsWith(".jar")) {
list.add(f.toURI().toURL());
}
}
}
return list;
}
}
Here's an example of the URL output added to the array:
file:/C:/my/path/to/dependent/jars/sqlite-jdbc-3.32.3.2.jar
file:/C:/my/path/to/main/app.jar
file: ... [10 more]
The URLClassloader seems to work well enough to load my main method in app.jar. The main executes a some startup type stuff, before attempting to load a login screen. When the request is made to get the user info database, my message screen loads and displays (<-this is important for later)
the stacktrace containing:
java.sql.SQLException: No suitable driver found for jdbc:sqlite:C:\...\users.db
I understand that this is because that jar is not on the class path, but it's loaded via the class loader, so why can't it find the classes from the jar? From this post JamesB suggested adding Class.forName("org.sqlite.JDBC"); before the connection request. I rebuilt the app jar with this line of code and it worked!
The weird thing that happened next, is that my message screen class can no longer be found even though earlier it loaded and displayed correctly. The message screen is a class inside my main app.jar and not in a dependent jar, which is why I'm baffled. Am I going to have to add Class.forName before every instance of any of my classes? That seems rude..
So what could I be doing wrong with the class loader? Why does it load some classes and not others despite that fact that all the jars have been added to the URL array?
Some other relative info: My app works perfectly as intended when launched from windows command line when the classpath is specified: java -cp "main-app.jar;my/dependent/jar/directory/*" main.AppStart. It's only when I try launching the app via this classloader that I have these issues.
By the way, is this java command universal? Will it work on all operating systems with java installed? If so, could I not just scrap this launcher, and use a process builder to execute the above command? Bonus points for someone who can tell me how to execute the command from a jre packaged with my app, as that's what I plan on doing so the user does not have to download Java.
EDIT
I figured out one of the answers to one of the questions below. Turns out, I didn't need to do any of the code below. My main method loads a login screen but after it's loaded it returns back to the AppLauncher code, thus closing the URLClassLoader! Of course, at that point any requested class will not be found as the loader has been closed! What an oof! Hopefully I will save someone a headache in the future...
Original
Well, after more time, effort, research, and effective use of Eclipse's debugging tool, I was able to figure out what I needed to do to resolve my issues.
So the first issue was my JDBC driver was never registered when passing the jars to the URLClassloader. This is the part I sorta don't understand, so advisement would be welcomed, but there is a static block in the JDBC class that registers the driver so it can be used by DriverManager see code below. Loading the class is what executes that static block, hence why calling Class.forName works.
static {
try {
DriverManager.registerDriver(new JDBC());
} catch (SQLException e) {
e.printStackTrace();
}
}
What I don't understand, is how class loading works if jars are specified via the class path. The URLClassLoader doesn't load any of those classes until they are called, and I never directly work with the JDBC class, thus no suitable driver exception, but are all the classes specified via the classpath loaded initially? Seems that way for static blocks to execute.
Anyhow, to resolve my other issue with some of my app's classes not being found I had to implement my own classloader. I get what I did and how it works well, but still don't understand why I had to do it. All of my jars were loaded to the original URLClassloader so if I could find them and the files within, why couldn't it do it?
Basically, I had to override the findClass and findResource methods to return jarEntry information that I had to store. I hope this code helps someone!
public class SBURLClassLoader extends URLClassLoader {
private HashMap<String, Storage> map;
public SBURLClassLoader(URL[] urls) {
super(urls);
map = new HashMap<>();
try {
storeClasses(urls);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private void storeClasses(URL[] urls) throws ClassNotFoundException {
for (URL u : urls) {
try {
JarFile jarFile = new JarFile(new File(u.getFile()));
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry jar = e.nextElement();
String entryName = jar.getName();
if (jar.isDirectory()) continue;
if (!entryName.endsWith(".class")) {
//still need to store these non-class files as resources
//let code continue to store entry un-altered
} else {
entryName = entryName.replace(".class", "");
entryName = entryName.replace("/", ".");
}
map.put(entryName, new Storage(jarFile, jar));
System.out.println(entryName);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
#Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = null;
try {
c = super.findClass(name);
} catch (ClassNotFoundException e) {
Storage s = map.get(name);
try {
InputStream in = s.jf.getInputStream(s.je);
int len = in.available();
c = defineClass(name, in.readAllBytes(), 0, len);
resolveClass(c);
} catch (IOException e1) {
e1.printStackTrace();
}
if (c == null) throw e;
}
return c;
}
#Override
public URL findResource(String name) {
URL url = super.findResource(name);
if (url == null) {
Storage s = map.get(name);
if (s != null) {
try {
url = new URL("jar:"+s.base.toString() + "!/" + name);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return url;
}
private class Storage {
public JarFile jf;
public JarEntry je;
public URL base;
public Storage(JarFile jf, JarEntry je) {
this.jf = jf;
this.je = je;
try {
base = Path.of(jf.getName()).toUri().toURL();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
}
Is it possible to access Assets inside the Java code in Play Framework? How?
We access assets from the scala HTML templates this way:
<img src="#routes.Assets.versioned("images/myimage.png")" width="800" />
But I could not find any documentation nor code example to do it from inside the Java code. I just found a controllers.Assets class but it is unclear how to use it. If this is the class that has to be used, should it maybe be injected?
I finally found a way to access the public folder even from a production mode application.
In order to be accessible/copied in the distributed version, public folder need to be mapped that way in build.sbt:
import NativePackagerHelper._
mappings in Universal ++= directory("public")
The files are then accessible in the public folder in the distributed app in production form the Java code:
private static final String PUBLIC_IMAGE_DIRECTORY_RELATIVE_PATH = "public/images/";
static File getImageAsset(String relativePath) throws ResourceNotFoundException {
final String path = PUBLIC_IMAGE_DIRECTORY_RELATIVE_PATH + relativePath;
final File file = new File(path);
if (!file.exists()) {
throw new ResourceNotFoundException(String.format("Asset %s not found", path));
}
return file;
}
This post put me on the right way to find the solution: https://groups.google.com/forum/#!topic/play-framework/sVDoEtAzP-U
The assets normally are in the "public" folder, and I don't know how you want to use your image so I have used ImageIO .
File file = new File("./public/images/nice.png");
boolean exists = file.exists();
String absolutePath = file.getAbsolutePath();
try {
ImageInputStream input = ImageIO.read(file); //Use it
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("EX = "+exists+" - "+absolutePath);
I'm programming an application which uses translation strings (.properties files); those strings are also made of non-latin characters (actually, they are copypastas like lenny smiles and stuff like this). My problem is, when I launch it inside my IDE Netbeans clicking on "clean and build" and then "run", everything goes fine and all the strings are displayed properly... the program acts as intended; but when I export my project as a fat jar (I use Maven shade plugin which also includes my properties files) and when I launch it double-clicking on its icon (still being on Windows) everything is messed up as some strings are not displayed:
inside my program, I use the class LocalisationService (code follows) to load strings inside different ArrayList. Running my project using Netbeans, strings are all loaded correctly (that's the supposed behaviour). Running my jar outside Netbeans, only 1/5 are loaded properly. I have a huge number of "String not found" entries in my arraylists and this means that, inside my LocalisationService class, method getString catches MissingResourceException exception. But actually those resources aren't missing, I mean, I include them properly in my Jar and Netbeans runs my project straight as it is supposed to do, so...
I have totally no idea on what could cause this issue: my IDE project encoding is set on UTF-8 so there shouldn't be any problems... Maven runs my project using:
cd C:\Users\utente\Documents\NetBeansProjects\mavenproject1; "JAVA_HOME=C:\\Program Files\\Java\\jdk1.8.0_40" cmd /c "\"\"C:\\Program Files\\NetBeans 8.0.2\\java\\maven\\bin\\mvn.bat\" -Dexec.args=\"-classpath %classpath bot.Main\" -Dexec.executable=\"C:\\Program Files\\Java\\jdk1.8.0_40\\bin\\java.exe\" -Dmaven.ext.class.path=\"C:\\Program Files\\NetBeans 8.0.2\\java\\maven-nblib\\netbeans-eventspy.jar\" -Dfile.encoding=UTF-8 org.codehaus.mojo:exec-maven-plugin:1.2.1:exec\""
I have the same problem on Ubuntu, strings are messed up launching my file from terminal with
/usr/bin/java -jar myproject.jar
Here is the class LocalisationService I use to retrieve the correct localisation string using a key:
public class LocalisationService {
private static LocalisationService instance = null;
private final HashMap<String, String> supportedLanguages = new HashMap<>();
private ResourceBundle english;
private ResourceBundle italian;
private class CustomClassLoader extends ClassLoader {
public CustomClassLoader(ClassLoader parent) {
super(parent);
}
public InputStream getResourceAsStream(String name) {
InputStream utf8in = getParent().getResourceAsStream(name);
if (utf8in != null) {
try {
byte[] utf8Bytes = new byte[utf8in.available()];
utf8in.read(utf8Bytes, 0, utf8Bytes.length);
byte[] iso8859Bytes = new String(utf8Bytes, "UTF-8").getBytes("ISO-8859-1");
return new ByteArrayInputStream(iso8859Bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
utf8in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
/**
* Singleton
* #return Instance of localisation service
*/
public static LocalisationService getInstance() {
if (instance == null) {
synchronized (LocalisationService.class) {
if (instance == null) {
instance = new LocalisationService();
}
}
}
return instance;
}
/**
* Private constructor due to singleton
*/
private LocalisationService() {
CustomClassLoader loader = new CustomClassLoader(Thread.currentThread().getContextClassLoader());
english = ResourceBundle.getBundle("localisation.strings", new Locale("en", "US"), loader);
supportedLanguages.put("en", "English");
italian = ResourceBundle.getBundle("localisation.strings", new Locale("it", "IT"), loader);
supportedLanguages.put("it", "Italiano");
}
/**
* Get a string in default language (en)
* #param key key of the resource to fetch
* #return fetched string or error message otherwise
*/
public String getString(String key) {
String result;
try {
result = english.getString(key);
} catch (MissingResourceException e) {
System.out.println("not found key... "+key);
result = "String not found";
}
return result;
}
/**
* Get a string in default language
* #param key key of the resource to fetch from localisations
* #param language code key for language (such as "EN" for english)
* #return fetched string or error message otherwise
*/
public String getString(String key, String language) {
String result;
try {
switch (language.toLowerCase()) {
case "en":
result = english.getString(key);
break;
case "it":
result = italian.getString(key);
break;
default:
result = english.getString(key);
break;
}
} catch (MissingResourceException e) {
result = english.getString(key);
}
return result;
}
public HashMap<String, String> getSupportedLanguages() {
return supportedLanguages;
}
public String getLanguageCodeByName(String language) {
return supportedLanguages.entrySet().stream().filter(x -> x.getValue().equals(language)).findFirst().get().getKey();
}
}
My project has no errors at all, neither warnings...
I also tried running my jar file using, on Ubuntu:
/usr/bin/java -Dfile.encoding=UTF-8 -jar myproject.jar
but still with no luck.
I really hope you guys may help me, I'm stuck on this issue since 2 days with no solutions at all...
Do not expect InputStream.available() to provide you with accurate information.
See the correct way to convert an InputStream to a ByteArrayInputStream here > Convert InputStream(Image) to ByteArrayInputStream
It seems clear that the InputStream provided by the Parent Classloader is somehow different when loaded in Netbeans than when run from your command line JVM implementation. Your code doesn't show the complete context that this code is executed from but it is possible that the InputStream implementation by Netbeans fully populated the available() method giving you the false impression that the code was correct.
See the documentation at https://docs.oracle.com/javase/7/docs/api/java/io/InputStream.html#available()
InputStream implementations are not required to populate this method with an accurate value so the results of your code will vary by JVM implementation.
I've tried to run a java applet using the javascript code in Eclipse IDE as shown in the web page Embedding Java Applet into .html file. But the output page shows error. My code to use applet is
<script src="//www.java.com/js/deployJava.js"></script>
in the head section and
<script>
var attributes = {
codebase : '../src/',
code : 'transfol.Main.class',
//archive: 'my-archive.jar',
width : '800',
height : '500'
};
var parameters = {
java_arguments : '-Xmx256m'
}; // customize per your needs
var version = '1.5'; // JDK version
deployJava.runApplet(attributes, parameters, version);
</script>
in the body section.
The way I've saved them is shown in the Navigator as
Main.class inside the package transfol which is in src folder (in Eclipse) and index.jsp in the web content
where Main.class is the applet and index.jsp is the file from which applet is being called.
I'm almost sure that the problem is in the codebase or code attributes where the path has to be specified, when I click on more information on applet, I get exception as:
The folloing exception has occured. For more information, try to launch the browser from the command line and examine the output.
For even more information you can visit http://icedtea.classpath.org/wiki/IcedTea-Web and follow the steps described there on how to obtain necessary information to file bug
Additional information may be available in the console or logs. Even more information is available if debugging is enabled.
Another available info:
IcedTea-Web Plugin version: 1.5 (1.5-1ubuntu1)
26/5/15 5:56 PM
Exception was:
net.sourceforge.jnlp.LaunchException: Fatal: Initialization Error: Could not initialize applet. For more information click "more information button".
at net.sourceforge.jnlp.Launcher.createApplet(Launcher.java:746)
at net.sourceforge.jnlp.Launcher.getApplet(Launcher.java:675)
at net.sourceforge.jnlp.Launcher$TgThread.run(Launcher.java:908)
Caused by: java.lang.ClassNotFoundException: Can't do a codebase look up and there are no jars. Failing sooner rather than later
at net.sourceforge.jnlp.Launcher.createApplet(Launcher.java:716)
... 2 more
This is the list of exceptions that occurred launching your applet. Please note, those exceptions can originate from multiple applets. For a helpful bug report, be sure to run only one applet.
1) at 26/5/15 5:47 PM
net.sourceforge.jnlp.LaunchException: Fatal: Initialization Error: Could not initialize applet. For more information click "more information button".
at net.sourceforge.jnlp.Launcher.createApplet(Launcher.java:746)
at net.sourceforge.jnlp.Launcher.getApplet(Launcher.java:675)
at net.sourceforge.jnlp.Launcher$TgThread.run(Launcher.java:908)
Caused by: java.lang.ClassNotFoundException: Can't do a codebase look up and there are no jars. Failing sooner rather than later
at net.sourceforge.jnlp.Launcher.createApplet(Launcher.java:716)
... 2 more
Try below code
<APPLET CODE=AppletSubclass.class WIDTH=anInt HEIGHT=anInt>
</APPLET>
OR
<object width="400" height="400" data="helloworld.class"></object>
Try this
Java applet
package cdig;
import java.applet.Applet;
import java.security.AccessController;
import java.security.PrivilegedAction;
public class CDigApplet extends Applet
{
private static final long serialVersionUID = 1L;
String ret;
CDigApplet applet = this;
#SuppressWarnings({ "rawtypes", "unchecked" })
public String signFile(String fileID, String pin, String token)
{
AccessController.doPrivileged(new PrivilegedAction()
{
#Override
public Object run()
{
try
{
System.out.println("Iniciando processo de assinatura.");
}
catch (Exception e)
{
String sl = "{\"success\":false," + "\"message\":\"" + e.getMessage() + "\"}";
ret = sl;
System.out.println(sl);
}
return null;
}
});
return ret;
}
public void init(){
}
public void destroy(){
}
}
HTML
<script>
<!-- applet id can be used to get a reference to the applet object -->
var attributes = { id:'cdigApplet', code:'cdig.CDigApplet', archive:'cdig-applet-1.0.jar', width:1, height:1, classloader_cache:'false'} ;
var parameters = {persistState: false, cache_option:'no' } ;
deployJava.runApplet(attributes, parameters, '1.8');
</script>
Call via javascript
var res = document.getElementById("cdigApplet").signFile(Id, '', token);
Don't forget to sign your applet and to not run your app with a URL with underscores '_' like this.
I'm trying to configure the Java Logging API's FileHandler to log my server to a file within a folder in my home directory, but I don't want to have to create those directories on every machine it's running.
For example in the logging.properties file I specify:
java.util.logging.FileHandler
java.util.logging.FileHandler.pattern=%h/app-logs/MyApplication/MyApplication_%u-%g.log
This would allow me to collect logs in my home directory (%h) for MyApplication and would rotate them (using the %u, and %g variables).
Log4j supports this when I specify in my log4j.properties:
log4j.appender.rolling.File=${user.home}/app-logs/MyApplication-log4j/MyApplication.log
It looks like there is a bug against the Logging FileHandler:
Bug 6244047: impossible to specify driectorys to logging FileHandler unless they exist
It sounds like they don't plan on fixing it or exposing any properties to work around the issue (beyond having your application parse the logging.properties or hard code the path needed):
It looks like the
java.util.logging.FileHandler does not
expect that the specified directory
may not exist. Normally, it has to
check this condition anyway. Also, it
has to check the directory writing
permissions as well. Another question
is what to do if one of these check
does not pass.
One possibility is to create the
missing directories in the path if the
user has proper permissions. Another
is to throw an IOException with a
clear message what is wrong. The
latter approach looks more consistent.
It seems like log4j version 1.2.15 does it.
Here is the snippet of the code which does it
public
synchronized
void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
throws IOException {
LogLog.debug("setFile called: "+fileName+", "+append);
// It does not make sense to have immediate flush and bufferedIO.
if(bufferedIO) {
setImmediateFlush(false);
}
reset();
FileOutputStream ostream = null;
try {
//
// attempt to create file
//
ostream = new FileOutputStream(fileName, append);
} catch(FileNotFoundException ex) {
//
// if parent directory does not exist then
// attempt to create it and try to create file
// see bug 9150
//
String parentName = new File(fileName).getParent();
if (parentName != null) {
File parentDir = new File(parentName);
if(!parentDir.exists() && parentDir.mkdirs()) {
ostream = new FileOutputStream(fileName, append);
} else {
throw ex;
}
} else {
throw ex;
}
}
Writer fw = createWriter(ostream);
if(bufferedIO) {
fw = new BufferedWriter(fw, bufferSize);
}
this.setQWForFiles(fw);
this.fileName = fileName;
this.fileAppend = append;
this.bufferedIO = bufferedIO;
this.bufferSize = bufferSize;
writeHeader();
LogLog.debug("setFile ended");
}
This piece of code is from FileAppender, RollingFileAppender extends FileAppender.
Here it is not checking whether we have permission to create the parent folders, but if the parent folders is not existing then it will try to create the parent folders.
EDITED
If you want some additional functionalily, you can always extend RollingFileAppender and override the setFile() method.
You can write something like this.
package org.log;
import java.io.IOException;
import org.apache.log4j.RollingFileAppender;
public class MyRollingFileAppender extends RollingFileAppender {
#Override
public synchronized void setFile(String fileName, boolean append,
boolean bufferedIO, int bufferSize) throws IOException {
//Your logic goes here
super.setFile(fileName, append, bufferedIO, bufferSize);
}
}
Then in your configuration
log4j.appender.fileAppender=org.log.MyRollingFileAppender
This works perfectly for me.
To work around the limitations of the Java Logging framework, and the unresolved bug: Bug 6244047: impossible to specify driectorys to logging FileHandler unless they exist
I've come up with 2 approaches (although only the first approach will actually work), both require your static void main() method for your app to initialize the logging system.
e.g.
public static void main(String[] args) {
initLogging();
...
}
The first approach hard-codes the log directories you expect to exist and creates them if they don't exist.
private static void initLogging() {
try {
//Create logging.properties specified directory for logging in home directory
//TODO: If they ever fix this bug (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6244047) in the Java Logging API we wouldn't need this hack
File homeLoggingDir = new File (System.getProperty("user.home")+"/webwars-logs/weblings-gameplatform/");
if (!homeLoggingDir.exists() ) {
homeLoggingDir.mkdirs();
logger.info("Creating missing logging directory: " + homeLoggingDir);
}
} catch(Exception e) {
e.printStackTrace();
}
try {
logger.info("[GamePlatform] : Starting...");
} catch (Exception exc) {
exc.printStackTrace();
}
}
The second approach could catch the IOException and create the directories listed in the exception, the problem with this approach is that the Logging framework has already failed to create the FileHandler so catching and resolving the error still leaves the logging system in a bad state.
As a possible solution I think there are 2 approaches (look at some of the previous answers). I can extend a Java Logging Handler class and write my own custom handler. I could also copy the log4j functionality and adapt it to the Java Logging framework.
Here's an example of copying the basic FileHandler and creating a CustomFileHandler see pastebin for full class:
The key is the openFiles() method where it tries to create a FileOutputStream and checking and creating the parent directory if it doesn't exist (I also had to copy package protected LogManager methods, why did they even make those package protected anyways):
// Private method to open the set of output files, based on the
// configured instance variables.
private void openFiles() throws IOException {
LogManager manager = LogManager.getLogManager();
...
// Create a lock file. This grants us exclusive access
// to our set of output files, as long as we are alive.
int unique = -1;
for (;;) {
unique++;
if (unique > MAX_LOCKS) {
throw new IOException("Couldn't get lock for " + pattern);
}
// Generate a lock file name from the "unique" int.
lockFileName = generate(pattern, 0, unique).toString() + ".lck";
// Now try to lock that filename.
// Because some systems (e.g. Solaris) can only do file locks
// between processes (and not within a process), we first check
// if we ourself already have the file locked.
synchronized (locks) {
if (locks.get(lockFileName) != null) {
// We already own this lock, for a different FileHandler
// object. Try again.
continue;
}
FileChannel fc;
try {
File lockFile = new File(lockFileName);
if (lockFile.getParent() != null) {
File lockParentDir = new File(lockFile.getParent());
// create the log dir if it does not exist
if (!lockParentDir.exists()) {
lockParentDir.mkdirs();
}
}
lockStream = new FileOutputStream(lockFileName);
fc = lockStream.getChannel();
} catch (IOException ix) {
// We got an IOException while trying to open the file.
// Try the next file.
continue;
}
try {
FileLock fl = fc.tryLock();
if (fl == null) {
// We failed to get the lock. Try next file.
continue;
}
// We got the lock OK.
} catch (IOException ix) {
// We got an IOException while trying to get the lock.
// This normally indicates that locking is not supported
// on the target directory. We have to proceed without
// getting a lock. Drop through.
}
// We got the lock. Remember it.
locks.put(lockFileName, lockFileName);
break;
}
}
...
}
I generally try to avoid static code but to work around this limitaton here is my approach that worked on my project just now.
I subclassed java.util.logging.FileHandler and implemented all constructors with their super calls. I put a static block of code in the class that creates the folders for my app in the user.home folder if they don't exist.
In my logging properties file I replaced java.util.logging.FileHandler with my new class.