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}