/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.felix.framework; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; import org.apache.felix.framework.util.SecureAction; import org.osgi.service.url.URLStreamHandlerService; import org.osgi.service.url.URLStreamHandlerSetter; /** * <p> * This class implements a stream handler proxy. When the stream handler * proxy instance is created, it is associated with a particular protocol * and will answer all future requests for handling of that stream type. It * does not directly handle the stream handler requests, but delegates the * requests to an underlying stream handler service. * </p> * <p> * The proxy instance for a particular protocol is used for all framework * instances that may contain their own stream handler services. When * performing a stream handler operation, the proxy retrieves the handler * service from the framework instance associated with the current call * stack and delegates the call to the handler service. * </p> * <p> * The proxy will create simple stream handler service trackers for each * framework instance. The trackers will listen to service events in its * respective framework instance to maintain a reference to the "best" * stream handler service at any given time. * </p> **/ public class URLHandlersStreamHandlerProxy extends URLStreamHandler implements URLStreamHandlerSetter, InvocationHandler { private static final Class[] URL_PROXY_CLASS; private static final Class[] STRING_TYPES = new Class[]{String.class}; private static final Method EQUALS; private static final Method GET_DEFAULT_PORT; private static final Method GET_HOST_ADDRESS; private static final Method HASH_CODE; private static final Method HOSTS_EQUAL; private static final Method OPEN_CONNECTION; private static final Method OPEN_CONNECTION_PROXY; private static final Method SAME_FILE; private static final Method TO_EXTERNAL_FORM; static { SecureAction action = new SecureAction(); try { EQUALS = URLStreamHandler.class.getDeclaredMethod("equals", new Class[]{URL.class, URL.class}); action.setAccesssible(EQUALS); GET_DEFAULT_PORT = URLStreamHandler.class.getDeclaredMethod("getDefaultPort", (Class[]) null); action.setAccesssible(GET_DEFAULT_PORT); GET_HOST_ADDRESS = URLStreamHandler.class.getDeclaredMethod( "getHostAddress", new Class[]{URL.class}); action.setAccesssible(GET_HOST_ADDRESS); HASH_CODE = URLStreamHandler.class.getDeclaredMethod( "hashCode", new Class[]{URL.class}); action.setAccesssible(HASH_CODE); HOSTS_EQUAL = URLStreamHandler.class.getDeclaredMethod( "hostsEqual", new Class[]{URL.class, URL.class}); action.setAccesssible(HOSTS_EQUAL); OPEN_CONNECTION = URLStreamHandler.class.getDeclaredMethod( "openConnection", new Class[]{URL.class}); action.setAccesssible(OPEN_CONNECTION); SAME_FILE = URLStreamHandler.class.getDeclaredMethod( "sameFile", new Class[]{URL.class, URL.class}); action.setAccesssible(SAME_FILE); TO_EXTERNAL_FORM = URLStreamHandler.class.getDeclaredMethod( "toExternalForm", new Class[]{URL.class}); action.setAccesssible(TO_EXTERNAL_FORM); } catch (Exception ex) { throw new RuntimeException(ex.getMessage()); } Method open_connection_proxy = null; Class[] url_proxy_class = null; try { url_proxy_class = new Class[]{URL.class, java.net.Proxy.class}; open_connection_proxy = URLStreamHandler.class.getDeclaredMethod( "openConnection", url_proxy_class); action.setAccesssible(open_connection_proxy); } catch (Throwable ex) { open_connection_proxy = null; url_proxy_class = null; } OPEN_CONNECTION_PROXY = open_connection_proxy; URL_PROXY_CLASS = url_proxy_class; } private final Object m_service; private final SecureAction m_action; private final URLStreamHandler m_builtIn; private final URL m_builtInURL; private final String m_protocol; public URLHandlersStreamHandlerProxy(String protocol, SecureAction action, URLStreamHandler builtIn, URL builtInURL) { m_protocol = protocol; m_service = null; m_action = action; m_builtIn = builtIn; m_builtInURL = builtInURL; } private URLHandlersStreamHandlerProxy(Object service, SecureAction action) { m_protocol = null; m_service = service; m_action = action; m_builtIn = null; m_builtInURL = null; } // // URLStreamHandler interface methods. // protected boolean equals(URL url1, URL url2) { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url1.getProtocol()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).equals(url1, url2); } try { return ((Boolean) EQUALS.invoke(svc, new Object[]{url1, url2})).booleanValue(); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } protected int getDefaultPort() { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException("Stream handler unavailable."); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).getDefaultPort(); } try { return ((Integer) GET_DEFAULT_PORT.invoke(svc, null)).intValue(); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } protected InetAddress getHostAddress(URL url) { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).getHostAddress(url); } try { return (InetAddress) GET_HOST_ADDRESS.invoke(svc, new Object[]{url}); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } protected int hashCode(URL url) { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).hashCode(url); } try { return ((Integer) HASH_CODE.invoke(svc, new Object[]{url})).intValue(); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } protected boolean hostsEqual(URL url1, URL url2) { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url1.getProtocol()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).hostsEqual(url1, url2); } try { return ((Boolean) HOSTS_EQUAL.invoke(svc, new Object[]{url1, url2})).booleanValue(); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } protected URLConnection openConnection(URL url) throws IOException { Object svc = getStreamHandlerService(); if (svc == null) { throw new MalformedURLException("Unknown protocol: " + url.toString()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).openConnection(url); } try { if ("http".equals(url.getProtocol()) && "felix.extensions".equals(url.getHost()) && 9 == url.getPort()) { try { Object handler = m_action.getDeclaredField( ExtensionManager.class, "m_extensionManager", null); if (handler != null) { return (URLConnection) m_action.invoke( m_action.getMethod(handler.getClass(), "openConnection", new Class[]{URL.class}), handler, new Object[]{url}); } throw new IOException("Extensions not supported or ambiguous context."); } catch (IOException ex) { throw ex; } catch (Exception ex) { throw new IOException(ex.getMessage()); } } return (URLConnection) OPEN_CONNECTION.invoke(svc, new Object[]{url}); } catch (IOException ex) { throw ex; } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } protected URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException { Object svc = getStreamHandlerService(); if (svc == null) { throw new MalformedURLException("Unknown protocol: " + url.toString()); } if (svc instanceof URLStreamHandlerService) { Method method; try { method = svc.getClass().getMethod("openConnection", URL_PROXY_CLASS); } catch (NoSuchMethodException e) { RuntimeException rte = new UnsupportedOperationException(e.getMessage()); rte.initCause(e); throw rte; } try { m_action.setAccesssible(method); return (URLConnection) method.invoke(svc, new Object[]{url, proxy}); } catch (Exception e) { if (e instanceof IOException) { throw (IOException) e; } throw new IOException(e.getMessage()); } } try { return (URLConnection) OPEN_CONNECTION_PROXY.invoke(svc, new Object[]{url, proxy}); } catch (Exception ex) { if (ex instanceof IOException) { throw (IOException) ex; } throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } // We use this thread local to detect whether we have a reentrant entry to the parseURL // method. This can happen do to some difference between gnu/classpath and sun jvms // For more see inside the method. private static final ThreadLocal m_loopCheck = new ThreadLocal(); protected void parseURL(URL url, String spec, int start, int limit) { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } if (svc instanceof URLStreamHandlerService) { ((URLStreamHandlerService) svc).parseURL(this, url, spec, start, limit); } else { try { URL test = null; // In order to cater for built-in urls being over-writable we need to use a // somewhat strange hack. We use a hidden feature inside the jdk which passes // the handler of the url given as a context to a new URL to that URL as its // handler. This way, we can create a new URL which will use the given built-in // handler to parse the url. Subsequently, we can use the information from that // URL to call set with the correct values. if (m_builtInURL != null) { // However, if we are on gnu/classpath we have to pass the handler directly // because the hidden feature is not there. Funnily, the workaround to pass // pass the handler directly doesn't work on sun as their handler detects // that it is not the same as the one inside the url and throws an exception // Luckily it doesn't do that on gnu/classpath. We detect that we need to // pass the handler directly by using the m_loopCheck thread local to detect // that we parseURL has been called inside a call to parseURL. if (m_loopCheck.get() != null) { test = new URL(new URL(m_builtInURL, url.toExternalForm()), spec, (URLStreamHandler) svc); } else { // Set-up the thread local as we don't expect to be called again until we are // done. Otherwise, we are on gnu/classpath m_loopCheck.set(Thread.currentThread()); try { test = new URL(new URL(m_builtInURL, url.toExternalForm()), spec); } finally { m_loopCheck.set(null); } } } else { // We don't have a url with a built-in handler for this but still want to create // the url with the buil-in handler as we could find one now. This might not // work for all handlers on sun but it is better then doing nothing. test = m_action.createURL(url, spec, (URLStreamHandler) svc); } super.setURL(url, test.getProtocol(), test.getHost(), test.getPort(),test.getAuthority(), test.getUserInfo(), test.getPath(), test.getQuery(), test.getRef()); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } } protected boolean sameFile(URL url1, URL url2) { Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url1.getProtocol()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).sameFile(url1, url2); } try { return ((Boolean) SAME_FILE.invoke( svc, new Object[]{url1, url2})).booleanValue(); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } public void setURL( URL url, String protocol, String host, int port, String authority, String userInfo, String path, String query, String ref) { super.setURL(url, protocol, host, port, authority, userInfo, path, query, ref); } public void setURL( URL url, String protocol, String host, int port, String file, String ref) { super.setURL(url, protocol, host, port, file, ref); } protected String toExternalForm(URL url) { return toExternalForm(url, getStreamHandlerService()); } private String toExternalForm(URL url, Object svc) { if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } if (svc instanceof URLStreamHandlerService) { return ((URLStreamHandlerService) svc).toExternalForm(url); } try { try { String result = (String) TO_EXTERNAL_FORM.invoke( svc, new Object[]{url}); // mika does return an invalid format if we have a url with the // protocol only (<proto>://null) - we catch this case now if ((result != null) && (result.equals(url.getProtocol() + "://null"))) { result = url.getProtocol() + ":"; } return result; } catch (InvocationTargetException ex) { Throwable t = ex.getTargetException(); if (t instanceof Exception) { throw (Exception) t; } else if (t instanceof Error) { throw (Error) t; } else { throw new IllegalStateException("Unknown throwable: " + t); } } } catch (NullPointerException ex) { // workaround for harmony and possibly J9. The issue is that // their implementation of URLStreamHandler.toExternalForm() // assumes that URL.getFile() doesn't return null but in our // case it can -- hence, we catch the NPE and do the work // ourselvs. The only difference is that we check whether the // URL.getFile() is null or not. StringBuilder answer = new StringBuilder(); answer.append(url.getProtocol()); answer.append(':'); String authority = url.getAuthority(); if ((authority != null) && (authority.length() > 0)) { answer.append("//"); //$NON-NLS-1$ answer.append(url.getAuthority()); } String file = url.getFile(); String ref = url.getRef(); if (file != null) { answer.append(file); } if (ref != null) { answer.append('#'); answer.append(ref); } return answer.toString(); } catch (Exception ex) { throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage()); } } /** * <p> * Private method to retrieve the stream handler service from the * framework instance associated with the current call stack. A * simple service tracker is created and cached for the associated * framework instance when this method is called. * </p> * @return the stream handler service from the framework instance * associated with the current call stack or <tt>null</tt> * is no service is available. **/ private Object getStreamHandlerService() { try { // Get the framework instance associated with call stack. Object framework = URLHandlers.getFrameworkFromContext(); if (framework == null) { return m_builtIn; } Object service; if (framework instanceof Felix) { service = ((Felix) framework).getStreamHandlerService(m_protocol); } else { service = m_action.invoke( m_action.getDeclaredMethod(framework.getClass(), "getStreamHandlerService", STRING_TYPES), framework, new Object[]{m_protocol}); } if (service == null) { return m_builtIn; } if (service instanceof URLStreamHandlerService) { return (URLStreamHandlerService) service; } return (URLStreamHandlerService) Proxy.newProxyInstance( URLStreamHandlerService.class.getClassLoader(), new Class[]{URLStreamHandlerService.class}, new URLHandlersStreamHandlerProxy(service, m_action)); } catch (ThreadDeath td) { throw td; } catch (Throwable t) { // In case that we are inside tomcat - the problem is that the webapp classloader // creates a new url to load a class. This gets us to this method. Now, if we // trigger a classload while executing, tomcat is creating a new url and we end-up with // a loop which is cut short after two iterations (because of a circularclassload). // We catch this exception (and all others) and just return the built-in handler // (if we have any) as this way we at least eventually get started (this just means // that we don't use the potentially provided built-in handler overwrite). return m_builtIn; } } public Object invoke(Object obj, Method method, Object[] params) throws Throwable { Class[] types = method.getParameterTypes(); if (m_service == null) { return m_action.invoke(m_action.getMethod(this.getClass(), method.getName(), types), this, params); } if ("parseURL".equals(method.getName())) { types[0] = m_service.getClass().getClassLoader().loadClass( URLStreamHandlerSetter.class.getName()); params[0] = Proxy.newProxyInstance( m_service.getClass().getClassLoader(), new Class[]{types[0]}, (URLHandlersStreamHandlerProxy) params[0]); } return m_action.invokeDirect(m_action.getDeclaredMethod(m_service.getClass(), method.getName(), types), m_service, params); } }