View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.io.channels;
19  
20  import java.lang.reflect.InvocationHandler;
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Proxy;
24  import java.nio.channels.AsynchronousChannel;
25  import java.nio.channels.ByteChannel;
26  import java.nio.channels.Channel;
27  import java.nio.channels.ClosedChannelException;
28  import java.nio.channels.GatheringByteChannel;
29  import java.nio.channels.InterruptibleChannel;
30  import java.nio.channels.NetworkChannel;
31  import java.nio.channels.ReadableByteChannel;
32  import java.nio.channels.ScatteringByteChannel;
33  import java.nio.channels.SeekableByteChannel;
34  import java.nio.channels.WritableByteChannel;
35  import java.util.Collections;
36  import java.util.HashSet;
37  import java.util.Objects;
38  import java.util.Set;
39  
40  /**
41   * An {@link InvocationHandler} supporting the implementation of {@link CloseShieldChannel}.
42   */
43  final class CloseShieldChannelHandler implements InvocationHandler {
44  
45      private static final Set<Class<? extends Channel>> SUPPORTED_INTERFACES;
46  
47      static {
48          final Set<Class<? extends Channel>> interfaces = new HashSet<>();
49          interfaces.add(AsynchronousChannel.class);
50          interfaces.add(ByteChannel.class);
51          interfaces.add(Channel.class);
52          interfaces.add(GatheringByteChannel.class);
53          interfaces.add(InterruptibleChannel.class);
54          interfaces.add(NetworkChannel.class);
55          interfaces.add(ReadableByteChannel.class);
56          interfaces.add(ScatteringByteChannel.class);
57          interfaces.add(SeekableByteChannel.class);
58          interfaces.add(WritableByteChannel.class);
59          SUPPORTED_INTERFACES = Collections.unmodifiableSet(interfaces);
60      }
61  
62      /**
63       * Tests whether the given method is allowed to be called after the shield is closed.
64       *
65       * @param declaringClass The class declaring the method.
66       * @param name           The method name.
67       * @param parameterCount The number of parameters.
68       * @return {@code true} if the method is allowed after {@code close()}, {@code false} otherwise.
69       */
70      private static boolean isAllowedAfterClose(final Class<?> declaringClass, final String name, final int parameterCount) {
71          // JDK explicitly allows NetworkChannel.supportedOptions() post-close
72          return parameterCount == 0 && name.equals("supportedOptions") && NetworkChannel.class.equals(declaringClass);
73      }
74  
75      static boolean isSupported(final Class<?> interfaceClass) {
76          return SUPPORTED_INTERFACES.contains(interfaceClass);
77      }
78  
79      /**
80       * Tests whether the given method returns 'this' (the channel) as per JDK spec.
81       *
82       * @param declaringClass The class declaring the method.
83       * @param name           The method name.
84       * @param parameterCount The number of parameters.
85       * @return {@code true} if the method returns 'this', {@code false} otherwise.
86       */
87      private static boolean returnsThis(final Class<?> declaringClass, final String name, final int parameterCount) {
88          if (SeekableByteChannel.class.equals(declaringClass)) {
89              // SeekableByteChannel.position(long) and truncate(long) return 'this'
90              return parameterCount == 1 && (name.equals("position") || name.equals("truncate"));
91          }
92          if (NetworkChannel.class.equals(declaringClass)) {
93              // NetworkChannel.bind and NetworkChannel.setOption returns 'this'
94              return parameterCount == 1 && name.equals("bind") || parameterCount == 2 && name.equals("setOption");
95          }
96          return false;
97      }
98  
99      private final Channel delegate;
100     private volatile boolean closed;
101 
102     CloseShieldChannelHandler(final Channel delegate) {
103         this.delegate = Objects.requireNonNull(delegate, "delegate");
104     }
105 
106     @Override
107     public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
108         final Class<?> declaringClass = method.getDeclaringClass();
109         final String name = method.getName();
110         final int parameterCount = method.getParameterCount();
111         // 1) java.lang.Object methods
112         if (declaringClass == Object.class) {
113             return invokeObjectMethod(proxy, method, args);
114         }
115         // 2) Channel.close(): mark shield closed, do NOT close the delegate
116         if (parameterCount == 0 && name.equals("close")) {
117             closed = true;
118             return null;
119         }
120         // 3) Channel.isOpen(): reflect shield state only
121         if (parameterCount == 0 && name.equals("isOpen")) {
122             return !closed && delegate.isOpen();
123         }
124         // 4) After the shield is closed, only allow a tiny allowlist of safe queries
125         if (closed && !isAllowedAfterClose(declaringClass, name, parameterCount)) {
126             throw new ClosedChannelException();
127         }
128         // 5) Delegate to the underlying channel and unwrap target exceptions
129         try {
130             final Object result = method.invoke(delegate, args);
131             return returnsThis(declaringClass, name, parameterCount) ? proxy : result;
132         } catch (final InvocationTargetException e) {
133             throw e.getCause();
134         }
135     }
136 
137     private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args) {
138         switch (method.getName()) {
139         case "toString":
140             return "CloseShieldChannel(" + delegate + ")";
141         case "hashCode":
142             return Objects.hashCode(delegate);
143         case "equals": {
144             final Object other = args[0];
145             if (other == null) {
146                 return false;
147             }
148             if (proxy == other) {
149                 return true;
150             }
151             if (Proxy.isProxyClass(other.getClass())) {
152                 final InvocationHandler h = Proxy.getInvocationHandler(other);
153                 if (h instanceof CloseShieldChannelHandler) {
154                     return Objects.equals(((CloseShieldChannelHandler) h).delegate, this.delegate);
155                 }
156             }
157             return false;
158         }
159         default:
160             // Not possible, all non-final Object methods are handled above.
161             return null;
162         }
163     }
164 }