001package uk.co.xfactorylibrarians.coremidi4j; 002 003import java.io.File; 004import java.io.FileOutputStream; 005import java.io.IOException; 006import java.io.InputStream; 007import java.net.URISyntaxException; 008import java.net.URL; 009 010/** 011 * Loads the native library when we are running on a Mac. If necessary, extracts a copy 012 * of the library from our jar file to a temporary directory, to save the user the trouble 013 * of having to install it on their system. Arranges for that directory to be deleted when 014 * we exit. 015 * 016 * <p>Inspired by the techniques used by usb4java, and the loader written by Klaus Raimer, k@ailis.de . 017 * See <a href="http://usb4java.org/" target="_blank">http://usb4java.org</a> for the web site 018 * for the project and <a href="https://github.com/usb4java/usb4java" target="_blank"> 019 * https://github.com/usb4java/usb4java</a> for the GitHub repository.</p> 020 * 021 * @author James Elliott 022 */ 023public class Loader { 024 025 /** 026 * How large a buffer should be used for copying the dynamic library out of the jar. 027 */ 028 029 private static final int BUFFER_SIZE = 8192; 030 031 /** 032 * The file name of our native library. 033 */ 034 035 public static final String NATIVE_LIBRARY_NAME = "libCoreMidi4J.dylib"; 036 037 /** 038 * Prevent instantiation. 039 */ 040 041 private Loader() { 042 043 // Nothing to do here 044 045 } 046 047 /** 048 * Check that we are running on Mac OS X. 049 * 050 * 051 * 052 * @return true if the OS is OS X, otherwise false 053 * 054 */ 055 056 private static boolean isMacOSX() { 057 058 final String os = System.getProperty("os.name").toLowerCase().replace(" ", ""); 059 060 return (os.equals("osx") || os.equals("macosx")); 061 062 } 063 064 /** 065 * The temporary directory we will use to extract the native library. 066 */ 067 068 private static File tempDir; 069 070 /** 071 * Indicates whether we have tried to load the library. 072 */ 073 074 private static boolean loaded = false; 075 076 /** 077 * Indicates whether the library was successfully loaded, which implies we are on a Mac and ready to operate. 078 */ 079 080 private static boolean available = false; 081 082 /** 083 * Creates the temporary directory used for unpacking the native library. 084 * This directory is marked to be deleted when the JVM exits. 085 * 086 *<p>This requires review. There is a method java.nio.file.Files.createTempDirectory 087 * that creates a temporary directory. However, 088 * java.io.File.createTempFile(String,String) creates a file in the 089 * default temp directory.</p> 090 * <p>This method creates a temporary directory named coreMidi4J in the default 091 * temp directory. Other code then places the dynamically linked library in 092 * the coreMidi4J directory. Both the dynamically linked library file and the 093 * directory use {@link java.io.File#deleteOnExit()}. This should work since they will be 094 * deleted in reverse order. However, unexpected crashes could leave the system 095 * in a bad state where some files are not deleted. 096 * </p> 097 * @return The temporary directory for the native library. 098 * 099 * @see java.io.File#delete() 100 * @see java.io.File#deleteOnExit() 101 * @throws CoreMidiException if there is a problem communicating with CoreMIDI 102 * 103 */ 104 105 private static File createTempDirectory() throws CoreMidiException { 106 107 if (tempDir != null) { 108 109 // We have already created it, so simply return it 110 return tempDir; 111 112 } 113 114 try { 115 116 tempDir = File.createTempFile("coreMidi4J", null); 117 118 if (!tempDir.delete()) { 119 120 throw new IOException("Unable to delete temporary file " + tempDir); 121 122 } if (!tempDir.mkdirs()) { 123 124 throw new IOException("Unable to create temporary directory " + tempDir); 125 126 } 127 128 tempDir.deleteOnExit(); 129 return tempDir; 130 131 } catch (final IOException e) { 132 133 throw new CoreMidiException("Unable to create temporary directory for CoreMidi4J library: " + e, e); 134 135 } 136 137 } 138 139 /** 140 * Copies the specified input stream to the specified output file. 141 * 142 * @param input The input stream to be copied. 143 * @param output The output file to which the stream should be copied. 144 * 145 * @throws IOException If the copy failed. 146 */ 147 private static void copy(final InputStream input, final File output) throws IOException { 148 149 final byte[] buffer = new byte[BUFFER_SIZE]; 150 151 try (FileOutputStream stream = new FileOutputStream(output)) { 152 153 int read; 154 155 while ((read = input.read(buffer)) != -1) { 156 157 stream.write(buffer, 0, read); 158 159 } 160 161 } 162 163 } 164 165 /** 166 * Locates the native library, extracting a temporary copy from our jar it does not already exist in the file system. 167 * 168 * @return The absolute path to the existing or extracted library. 169 * 170 * @throws CoreMidiException if there is a problem finding the library 171 */ 172 173 private static String locateLibrary() throws CoreMidiException { 174 175 // Check if native library is present 176 final String source = "/uk/co/xfactorylibrarians/coremidi4j/" + NATIVE_LIBRARY_NAME; 177 final URL url = Loader.class.getResource(source); 178 179 if (url == null ) { 180 181 throw new CoreMidiException("Native library " + source + " not found in classpath."); 182 183 } 184 185 // If the native library was found as an actual file, there is no need to extract a copy from our jar. 186 if ( "file".equals(url.getProtocol()) ) { 187 188 try { 189 190 return new File(url.toURI()).getAbsolutePath(); 191 192 } catch (final URISyntaxException e) { 193 194 // In theory this can't happen because we are not constructing the URI manually. 195 // But even if it happens, we can fall back to extracting the library. 196 System.err.println("Problem trying to obtain File from dynamic library: " + e); 197 198 } 199 200 } 201 202 // Extract the library and return the path to the extracted file. 203 final File dest = new File(createTempDirectory(), NATIVE_LIBRARY_NAME); 204 205 try { 206 207 final InputStream stream = Loader.class.getResourceAsStream(source); 208 209 if (stream == null) { 210 211 throw new CoreMidiException("Unable to find " + source + " in the classpath"); 212 213 } 214 215 try { 216 217 copy(stream, dest); 218 219 } finally { 220 221 stream.close(); 222 223 } 224 225 } catch (final IOException e) { 226 227 throw new CoreMidiException("Unable to extract native library " + source + " to " + dest + ": " + e, e); 228 229 } 230 231 // Arrange for the copied library to be deleted when the JVM exits 232 dest.deleteOnExit(); 233 234 return dest.getAbsolutePath(); 235 236 } 237 238 /** 239 * Tries to load our native library. Can be safely called multiple times; duplicate attempts are ignored. 240 * This method is automatically called whenever any CoreMidi4J class that relies on JNI is loaded. If you 241 * need to do it earlier (to catch exceptions for example, or check whether the native library can be used), 242 * simply call this method manually. 243 * 244 * @throws CoreMidiException if something unexpected happens trying to load the native library on a Mac OS X system. 245 */ 246 247 public static synchronized void load() throws CoreMidiException { 248 249 // Do nothing if this is a redundant call 250 if (loaded) { 251 252 return; 253 254 } 255 256 loaded = true; 257 258 if ( isMacOSX() ) { 259 260 System.load(locateLibrary()); 261 available = true; 262 263 } 264 265 } 266 267 /** 268 * Checks whether CoreMidi4J is available on the current system, in other words whether it is a Mac OS X system 269 * and the native library was loaded successfully. Will attempt to perform that load if it has not yet occurred. 270 * 271 * @return true if this is a Mac OS X system and the native library has been loaded successfully. 272 * 273 * @throws CoreMidiException if something unexpected happens trying to load the native library on a Mac OS X system. 274 */ 275 public static synchronized boolean isAvailable() throws CoreMidiException { 276 277 load(); 278 return available; 279 280 } 281 282}