Now, let’s see a function that calls this move function.
package org.qoolloop.sample.exceptionhandling; import java.io.File; import org.qoolloop.exception.UnrecoveredException; import org.qoolloop.logging.Logger; public class MakeFile { ・・・ /** * Save to file. * * @param i_filename name of file to save to. * @throws RecoveredException_CannotWriteToDirectory Cannot specify directory for i_filename. * @throws RecoveredException_IO An I/O exception occurred during file manipulation. * @throws RecoveredException_CannotWriteToFile Cannot write to i_filename for some reason. * @throws RecoveredException_IllegalStorageManipulation Files that should not have been touched were manipulated by an external entity during processing. * @throws UnrecoveredException_IO An I/O exception occurred, and storage may not have been recovered. * */ public void saveToFile(String i_filename) throws RecoveredException_CannotWriteToDirectory, RecoveredException_IO, RecoveredException_CannotWriteToFile, UnrecoveredException_IO, RecoveredException_IllegalStorageManipulation { m_logger.arguments(i_filename); File destinationFile = new File(i_filename); boolean destinationFileExisted = destinationFile.exists(); if (destinationFile.isDirectory()) { throw new RecoveredException_CannotWriteToDirectory(null, destinationFile); } File temporaryFile = FileManipulation.createTempFile("MakeFile", ".tmp"); // throws RecoveredException_IO try { try { actuallySaveToFile(temporaryFile); } catch (UnrecoveredException c_exception) { temporaryFile.delete(); throw c_exception; } } catch (UnrecoveredException_IO c_exception) { if (temporaryFile.exists()) { throw c_exception; } else { throw new RecoveredException_IO(c_exception, temporaryFile); } } try { try { FileManipulation.move(temporaryFile.getAbsolutePath(), destinationFile.getAbsolutePath()); } catch (UnrecoveredException c_exception) { temporaryFile.delete(); throw c_exception; } } catch (RecoveredException_NoSuchFile | RecoveredException_SourceNotFile | RecoveredException_DirectoryNotEmpty c_exception) { throw new RecoveredException_IllegalStorageManipulation(c_exception, temporaryFile); } catch (UnrecoveredException_IO c_exception) { if (destinationFileExisted) { throw c_exception; } else { destinationFile.delete(); if (destinationFile.exists()) { throw c_exception; } else { throw new RecoveredException_IO(c_exception, c_exception.getFilesCausingException()); } } } } } The overall flow of the saveToFile() function is:
In the beginning of the function, the argument values are logged with m_logger.arguments(). If this is put in a catch block, logging arguments will be performed only when an exception occurs [i]. The next part of the function checks whether the specified file name (i_filename) points to a directory. This is checked, because the FileManipulation.move() called later will fail if the destination is a directory. A RecoveredException is thown immediately, since nothing permanent has been done yet. FileManipulation.createTempFile() is a function that creates a temporary file. It just wraps the java.io.File.createTempFile(), so that it throws exceptions defined in the package, instead of the exceptions standard in the language. If a RecoveredException were to be thrown from this function, it can be propagated to the caller of saveToFile(), since saveToFile() hasn’t made any permanent modifications. The next part calls actuallySaveToFile(), where data is stored to the temporary file, and the last part calls FileManipulation.move(), so that the temporary file becomes the output of this saveToFile() function. In the last two parts, the functions are surrounded by two nested try-catch blocks. This would be the typical structure for exception handling. The inner block handles the recovery, and the outer block handles the conversion of exceptions. Many people say that sharing a common procedure between exception handlers is not possible without defining functions, but it is also possible by nesting try-catch blocks. This is possible because of the class hierarchy that we defined. The two calls to actuallySaveFile() and FileManipulation.move() are wrapped in a try-catch block that catches UnrecoveredException_IO and converts it to a RecoveredException_IO, if recovery succeeds. It isn’t always the case that an UnrecoveredException results in another UnrecoveredException being thrown. If recovery is possible, it can be converted to a RecoveredException. One cautionary point with this approach is that, as a rule, you shouldn’t forget to catch RecoveredExceptions in functions with side effects [ii]. Usually, when an exception is thrown midway in a function, there is some permanent change done. So, even if the callee raised a RecoveredException, it doesn’t mean the caller can rethrow that exception without performing any recovery. In all of these cases, the constructor of RecoveredException and FatalException will record its creation to the log. [i] In the case of Java, I intend to use annotations for some of this exception handling, but I haven’t come to that yet. [ii] A function with side effects is a function that does more than just calculate values. They may change member fields in a class instance. They may save data in a file. They might communicate through the network. Functions with side effects should never be used in assertions. |
Contents |