001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.felix.framework.cache;
020    
021    import java.io.BufferedInputStream;
022    import java.io.ByteArrayOutputStream;
023    import java.io.File;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.util.Enumeration;
027    import java.util.HashMap;
028    import java.util.Map;
029    import java.util.Properties;
030    import java.util.zip.ZipEntry;
031    import org.apache.felix.framework.Logger;
032    import org.apache.felix.framework.util.FelixConstants;
033    import org.apache.felix.framework.util.JarFileX;
034    import org.apache.felix.framework.util.Util;
035    import org.apache.felix.moduleloader.IContent;
036    import org.osgi.framework.Constants;
037    
038    public class JarContent implements IContent
039    {
040        private static final int BUFSIZE = 4096;
041        private static final transient String EMBEDDED_DIRECTORY = "-embedded";
042        private static final transient String LIBRARY_DIRECTORY = "-lib";
043    
044        private final Logger m_logger;
045        private final Map m_configMap;
046        private final Object m_revisionLock;
047        private final File m_rootDir;
048        private final File m_file;
049        private final JarFileX m_jarFile;
050        private final boolean m_isJarFileOwner;
051        private Map m_nativeLibMap;
052    
053        public JarContent(Logger logger, Map configMap, Object revisionLock, File rootDir,
054            File file, JarFileX jarFile)
055        {
056            m_logger = logger;
057            m_configMap = configMap;
058            m_revisionLock = revisionLock;
059            m_rootDir = rootDir;
060            m_file = file;
061            m_jarFile = (jarFile == null) ? openJarFile(m_file) : jarFile;
062            m_isJarFileOwner = (jarFile == null);
063        }
064    
065        protected void finalize()
066        {
067            close();
068        }
069    
070        public void close()
071        {
072            try
073            {
074                if (m_isJarFileOwner)
075                {
076                    m_jarFile.close();
077                }
078            }
079            catch (Exception ex)
080            {
081                m_logger.log(
082                    Logger.LOG_ERROR,
083                    "JarContent: Unable to close JAR file.", ex);
084            }
085        }
086    
087        public boolean hasEntry(String name) throws IllegalStateException
088        {
089            try
090            {
091                ZipEntry ze = m_jarFile.getEntry(name);
092                return ze != null;
093            }
094            catch (Exception ex)
095            {
096                return false;
097            }
098            finally
099            {
100            }
101        }
102    
103        public Enumeration getEntries()
104        {
105            // Wrap entries enumeration to filter non-matching entries.
106            Enumeration e = new EntriesEnumeration(m_jarFile.entries());
107    
108            // Spec says to return null if there are no entries.
109            return (e.hasMoreElements()) ? e : null;
110        }
111    
112        public byte[] getEntryAsBytes(String name) throws IllegalStateException
113        {
114            // Get the embedded resource.
115            InputStream is = null;
116            ByteArrayOutputStream baos = null;
117    
118            try
119            {
120                ZipEntry ze = m_jarFile.getEntry(name);
121                if (ze == null)
122                {
123                    return null;
124                }
125                is = m_jarFile.getInputStream(ze);
126                if (is == null)
127                {
128                    return null;
129                }
130                baos = new ByteArrayOutputStream(BUFSIZE);
131                byte[] buf = new byte[BUFSIZE];
132                int n = 0;
133                while ((n = is.read(buf, 0, buf.length)) >= 0)
134                {
135                    baos.write(buf, 0, n);
136                }
137                return baos.toByteArray();
138    
139            }
140            catch (Exception ex)
141            {
142                m_logger.log(
143                    Logger.LOG_ERROR,
144                    "JarContent: Unable to read bytes.", ex);
145                return null;
146            }
147            finally
148            {
149                try
150                {
151                    if (baos != null) baos.close();
152                }
153                catch (Exception ex)
154                {
155                }
156                try
157                {
158                    if (is != null) is.close();
159                }
160                catch (Exception ex)
161                {
162                }
163            }
164        }
165    
166        public InputStream getEntryAsStream(String name)
167            throws IllegalStateException, IOException
168        {
169            // Get the embedded resource.
170            InputStream is = null;
171    
172            try
173            {
174                ZipEntry ze = m_jarFile.getEntry(name);
175                if (ze == null)
176                {
177                    return null;
178                }
179                is = m_jarFile.getInputStream(ze);
180                if (is == null)
181                {
182                    return null;
183                }
184            }
185            catch (Exception ex)
186            {
187                return null;
188            }
189    
190            return is;
191        }
192    
193        public IContent getEntryAsContent(String entryName)
194        {
195            // If the entry name refers to the content itself, then
196            // just return it immediately.
197            if (entryName.equals(FelixConstants.CLASS_PATH_DOT))
198            {
199                return new JarContent(m_logger, m_configMap, m_revisionLock,
200                    m_rootDir, m_file, m_jarFile);
201            }
202    
203            // Remove any leading slash.
204            entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName;
205    
206            // Any embedded JAR files will be extracted to the embedded directory.
207            // Since embedded JAR file names may clash when extracting from multiple
208            // embedded JAR files, the embedded directory is per embedded JAR file.
209            File embedDir = new File(m_rootDir, m_file.getName() + EMBEDDED_DIRECTORY);
210    
211            // Find the entry in the JAR file and create the
212            // appropriate content type for it.
213    
214            // Determine if the entry is an emdedded JAR file or
215            // directory in the bundle JAR file. Ignore any entries
216            // that do not exist per the spec.
217            ZipEntry ze = m_jarFile.getEntry(entryName);
218            if ((ze != null) && ze.isDirectory())
219            {
220                File extractDir = new File(embedDir, entryName);
221    
222                // Extracting an embedded directory file impacts all other existing
223                // contents for this revision, so we have to grab the revision
224                // lock first before trying to create a directory for an embedded
225                // directory to avoid a race condition.
226                synchronized (m_revisionLock)
227                {
228                    if (!BundleCache.getSecureAction().fileExists(extractDir))
229                    {
230                        if (!BundleCache.getSecureAction().mkdirs(extractDir))
231                        {
232                            m_logger.log(
233                                Logger.LOG_ERROR,
234                                "Unable to extract embedded directory.");
235                        }
236                    }
237                }
238                return new ContentDirectoryContent(this, entryName);
239            }
240            else if ((ze != null) && ze.getName().endsWith(".jar"))
241            {
242                File extractJar = new File(embedDir, entryName);
243    
244                // Extracting the embedded JAR file impacts all other existing
245                // contents for this revision, so we have to grab the revision
246                // lock first before trying to extract the embedded JAR file
247                // to avoid a race condition.
248                synchronized (m_revisionLock)
249                {
250                    if (!BundleCache.getSecureAction().fileExists(extractJar))
251                    {
252                        try
253                        {
254                            extractEmbeddedJar(entryName);
255                        }
256                        catch (Exception ex)
257                        {
258                            m_logger.log(
259                                Logger.LOG_ERROR,
260                                "Unable to extract embedded JAR file.", ex);
261                        }
262                    }
263                }
264                return new JarContent(
265                    m_logger, m_configMap, m_revisionLock,
266                    extractJar.getParentFile(), extractJar, null);
267            }
268    
269            // The entry could not be found, so return null.
270            return null;
271        }
272    
273    // TODO: This will need to consider security.
274        public String getEntryAsNativeLibrary(String entryName)
275        {
276            // Return result.
277            String result = null;
278    
279            // Remove any leading slash.
280            entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName;
281    
282            // Any embedded native libraries will be extracted to the lib directory.
283            // Since embedded library file names may clash when extracting from multiple
284            // embedded JAR files, the embedded lib directory is per embedded JAR file.
285            File libDir = new File(m_rootDir, m_file.getName() + LIBRARY_DIRECTORY);
286    
287            // The entry name must refer to a file type, since it is
288            // a native library, not a directory.
289            ZipEntry ze = m_jarFile.getEntry(entryName);
290            if ((ze != null) && !ze.isDirectory())
291            {
292                // Extracting the embedded native library file impacts all other
293                // existing contents for this revision, so we have to grab the
294                // revision lock first before trying to extract the embedded JAR
295                // file to avoid a race condition.
296                synchronized (m_revisionLock)
297                {
298                    // Since native libraries cannot be shared, we must extract a
299                    // separate copy per request, so use the request library counter
300                    // as part of the extracted path.
301                    if (m_nativeLibMap == null)
302                    {
303                        m_nativeLibMap = new HashMap();
304                    }
305                    Integer libCount = (Integer) m_nativeLibMap.get(entryName);
306                    // Either set or increment the library count.
307                    libCount = (libCount == null) ? new Integer(0) : new Integer(libCount.intValue() + 1);
308                    m_nativeLibMap.put(entryName, libCount);
309                    File libFile = new File(
310                        libDir, libCount.toString() + File.separatorChar + entryName);
311    
312                    if (!BundleCache.getSecureAction().fileExists(libFile))
313                    {
314                        if (!BundleCache.getSecureAction().fileExists(libFile.getParentFile())
315                            && !BundleCache.getSecureAction().mkdirs(libFile.getParentFile()))
316                        {
317                            m_logger.log(
318                                Logger.LOG_ERROR,
319                                "Unable to create library directory.");
320                        }
321                        else
322                        {
323                            InputStream is = null;
324    
325                            try
326                            {
327                                is = new BufferedInputStream(
328                                    m_jarFile.getInputStream(ze),
329                                    BundleCache.BUFSIZE);
330                                if (is == null)
331                                {
332                                    throw new IOException("No input stream: " + entryName);
333                                }
334    
335                                // Create the file.
336                                BundleCache.copyStreamToFile(is, libFile);
337    
338                                // Perform exec permission command on extracted library
339                                // if one is configured.
340                                String command = (String) m_configMap.get(
341                                    Constants.FRAMEWORK_EXECPERMISSION);
342                                if (command != null)
343                                {
344                                    Properties props = new Properties();
345                                    props.setProperty("abspath", libFile.toString());
346                                    command = Util.substVars(command, "command", null, props);
347                                    Process p = BundleCache.getSecureAction().exec(command);
348                                    // We have to make sure we read stdout and stderr because
349                                    // otherwise we will block on certain unbuffered os's
350                                    // (like eg. windows)
351                                    Thread stdOut = new Thread(
352                                        new DevNullRunnable(p.getInputStream()));
353                                    Thread stdErr = new Thread(
354                                        new DevNullRunnable(p.getErrorStream()));
355                                    stdOut.setDaemon(true);
356                                    stdErr.setDaemon(true);
357                                    stdOut.start();
358                                    stdErr.start();
359                                    p.waitFor();
360                                    stdOut.join();
361                                    stdErr.join();
362                                }
363    
364                                // Return the path to the extracted native library.
365                                result = BundleCache.getSecureAction().getAbsolutePath(libFile);
366                            }
367                            catch (Exception ex)
368                            {
369                                m_logger.log(
370                                    Logger.LOG_ERROR,
371                                    "Extracting native library.", ex);
372                            }
373                            finally
374                            {
375                                try
376                                {
377                                    if (is != null) is.close();
378                                }
379                                catch (IOException ex)
380                                {
381                                    // Not much we can do.
382                                }
383                            }
384                        }
385                    }
386                    else
387                    {
388                        // Return the path to the extracted native library.
389                        result = BundleCache.getSecureAction().getAbsolutePath(libFile);
390                    }
391                }
392            }
393    
394            return result;
395        }
396    
397        public String toString()
398        {
399            return "JAR " + m_file.getPath();
400        }
401    
402        public File getFile()
403        {
404            return m_file;
405        }
406    
407        /**
408         * This method extracts an embedded JAR file from the bundle's
409         * JAR file.
410         * @param id the identifier of the bundle that owns the embedded JAR file.
411         * @param jarPath the path to the embedded JAR file inside the bundle JAR file.
412        **/
413        private void extractEmbeddedJar(String jarPath)
414            throws Exception
415        {
416            // Remove leading slash if present.
417            jarPath = (jarPath.length() > 0) && (jarPath.charAt(0) == '/')
418                ? jarPath.substring(1) : jarPath;
419    
420            // Any embedded JAR files will be extracted to the embedded directory.
421            // Since embedded JAR file names may clash when extracting from multiple
422            // embedded JAR files, the embedded directory is per embedded JAR file.
423            File embedDir = new File(m_rootDir, m_file.getName() + EMBEDDED_DIRECTORY);
424            File jarFile = new File(embedDir, jarPath);
425    
426            if (!BundleCache.getSecureAction().fileExists(jarFile))
427            {
428                InputStream is = null;
429                try
430                {
431                    // Make sure class path entry is a JAR file.
432                    ZipEntry ze = m_jarFile.getEntry(jarPath);
433                    if (ze == null)
434                    {
435                        return;
436                    }
437                    // If the zip entry is a directory, then ignore it since
438                    // we don't need to extact it; otherwise, it points to an
439                    // embedded JAR file, so extract it.
440                    else if (!ze.isDirectory())
441                    {
442                        // Make sure that the embedded JAR's parent directory exists;
443                        // it may be in a sub-directory.
444                        File jarDir = jarFile.getParentFile();
445                        if (!BundleCache.getSecureAction().fileExists(jarDir))
446                        {
447                            if (!BundleCache.getSecureAction().mkdirs(jarDir))
448                            {
449                                throw new IOException("Unable to create embedded JAR directory.");
450                            }
451                        }
452    
453                        // Extract embedded JAR into its directory.
454                        is = new BufferedInputStream(m_jarFile.getInputStream(ze), BundleCache.BUFSIZE);
455                        if (is == null)
456                        {
457                            throw new IOException("No input stream: " + jarPath);
458                        }
459                        // Copy the file.
460                        BundleCache.copyStreamToFile(is, jarFile);
461                    }
462                }
463                finally
464                {
465                    if (is != null) is.close();
466                }
467            }
468        }
469    
470        private static JarFileX openJarFile(File file) throws RuntimeException
471        {
472            try
473            {
474                return BundleCache.getSecureAction().openJAR(file, false);
475            }
476            catch (IOException ex)
477            {
478                throw new RuntimeException(
479                    "Unable to open JAR file, probably deleted: " + ex.getMessage());
480            }
481        }
482    
483        private static class EntriesEnumeration implements Enumeration
484        {
485            private Enumeration m_enumeration = null;
486    
487            public EntriesEnumeration(Enumeration enumeration)
488            {
489                m_enumeration = enumeration;
490            }
491    
492            public boolean hasMoreElements()
493            {
494                return m_enumeration.hasMoreElements();
495            }
496    
497            public Object nextElement()
498            {
499                return ((ZipEntry) m_enumeration.nextElement()).getName();
500            }
501        }
502    
503        private static class DevNullRunnable implements Runnable 
504        {
505            private final InputStream m_in;
506    
507            public DevNullRunnable(InputStream in) 
508            {
509                m_in = in;
510            }
511    
512            public void run()
513            {
514                try
515                {
516                    try
517                    {
518                        while (m_in.read() != -1){}
519                    }
520                    finally 
521                    {
522                        m_in.close();
523                    }
524                }
525                catch (Exception ex) 
526                {
527                    // Not much we can do - maybe we should log it?
528                }
529            }
530        }
531    }