Skip to content

Commit 37cab2b

Browse files
committed
Add tests
- Remove unused exception from signature of CloseShieldChannelHandler.invokeObjectMethod(Object, Method, Object[]) - Test AsynchronousChannel - Test AsynchronousByteChannel - Test NetworkChannel - Test MulticastChannel - Test InterruptibleChannel - Add @SuppressWarnings value - Inline single use local variable - Javadoc - Sort members - Use final
1 parent b6d09eb commit 37cab2b

File tree

4 files changed

+197
-189
lines changed

4 files changed

+197
-189
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The <action> type attribute can be add,update,fix,remove.
6666
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add org.apache.commons.io.file.PathUtils.getPath(String, String).</action>
6767
<action dev="ggregory" type="add" due-to="Gary Gregory">Add org.apache.commons.io.channels.ByteArraySeekableByteChannel.</action>
6868
<action dev="ggregory" type="add" due-to="Gary Gregory">Add IOIterable.asIterable().</action>
69-
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add Channels.closeShield(Channel) for close-shielded NIO Channel proxies.</action>
69+
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add CloseShieldChannel to close-shielded NIO Channels.</action>
7070
<!-- UPDATE -->
7171
<action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 85 to 88 #774, #783.</action>
7272
<action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0.</action>

src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,42 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
1718
package org.apache.commons.io.channels;
1819

19-
import java.lang.reflect.InvocationHandler;
20+
import java.io.Closeable;
2021
import java.lang.reflect.Proxy;
2122
import java.nio.channels.Channel;
2223
import java.util.LinkedHashSet;
2324
import java.util.Objects;
2425
import java.util.Set;
2526

2627
/**
27-
* Utility to create a close-shielding proxy for a {@link Channel}.
28+
* Creates a close-shielding proxy for a {@link Channel}.
2829
*
29-
* <p>The returned proxy will implement all {@link Channel} sub-interfaces that the delegate implements.</p>
30+
* <p>
31+
* The returned proxy will implement all {@link Channel} sub-interfaces that the delegate implements.
32+
* </p>
3033
*
34+
* @see Channel
35+
* @see Closeable
3136
* @since 2.21.0
3237
*/
3338
public final class CloseShieldChannel {
3439

35-
private CloseShieldChannel() {
36-
// no instance
40+
private static Class<?>[] collectChannelInterfaces(final Class<?> type) {
41+
final Set<Class<?>> out = new LinkedHashSet<>();
42+
collectChannelInterfaces(type, out);
43+
return out.toArray(new Class<?>[0]);
44+
}
45+
46+
private static void collectChannelInterfaces(final Class<?> type, final Set<Class<?>> out) {
47+
// Visit interfaces
48+
for (final Class<?> iface : type.getInterfaces()) {
49+
if (Channel.class.isAssignableFrom(iface) && out.add(iface)) {
50+
collectChannelInterfaces(iface, out);
51+
}
52+
}
3753
}
3854

3955
/**
@@ -43,42 +59,25 @@ private CloseShieldChannel() {
4359
* @param <T> Any Channel type (interface or class).
4460
* @return A proxy that shields {@code close()} and enforces closed semantics on other calls.
4561
*/
46-
@SuppressWarnings("unchecked")
62+
@SuppressWarnings({ "unchecked", "resource" }) // caller closes
4763
public static <T extends Channel> T wrap(final T channel) {
4864
Objects.requireNonNull(channel, "channel");
49-
5065
// Fast path: already our shield
5166
if (Proxy.isProxyClass(channel.getClass())) {
52-
final InvocationHandler handler = Proxy.getInvocationHandler(channel);
53-
if (handler instanceof CloseShieldChannelHandler) {
67+
if (Proxy.getInvocationHandler(channel) instanceof CloseShieldChannelHandler) {
5468
return channel;
5569
}
5670
}
57-
5871
// Collect only Channel sub-interfaces.
5972
Class<?>[] ifaces = collectChannelInterfaces(channel.getClass());
6073
if (ifaces.length == 0) {
61-
ifaces = new Class<?>[] {Channel.class}; // fallback to minimal surface
74+
ifaces = new Class<?>[] { Channel.class }; // fallback to root surface
6275
}
63-
64-
return (T) Proxy.newProxyInstance(
65-
channel.getClass().getClassLoader(), // use delegate's loader
66-
ifaces,
67-
new CloseShieldChannelHandler(channel));
68-
}
69-
70-
private static Class<?>[] collectChannelInterfaces(final Class<?> type) {
71-
final Set<Class<?>> out = new LinkedHashSet<>();
72-
collectChannelInterfaces(type, out);
73-
return out.toArray(new Class<?>[0]);
76+
return (T) Proxy.newProxyInstance(channel.getClass().getClassLoader(), // use delegate's loader
77+
ifaces, new CloseShieldChannelHandler(channel));
7478
}
7579

76-
private static void collectChannelInterfaces(final Class<?> type, final Set<Class<?>> out) {
77-
// Visit interfaces
78-
for (Class<?> iface : type.getInterfaces()) {
79-
if (Channel.class.isAssignableFrom(iface) && out.add(iface)) {
80-
collectChannelInterfaces(iface, out);
81-
}
82-
}
80+
private CloseShieldChannel() {
81+
// no instance
8382
}
8483
}

src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java

Lines changed: 57 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
1718
package org.apache.commons.io.channels;
1819

1920
import java.lang.reflect.InvocationHandler;
@@ -28,6 +29,39 @@
2829

2930
final class CloseShieldChannelHandler implements InvocationHandler {
3031

32+
/**
33+
* Tests whether the given method is allowed to be called after the shield is closed.
34+
*
35+
* @param declaringClass The class declaring the method.
36+
* @param name The method name.
37+
* @param parameterCount The number of parameters.
38+
* @return {@code true} if the method is allowed after {@code close()}, {@code false} otherwise.
39+
*/
40+
private static boolean isAllowedAfterClose(final Class<?> declaringClass, final String name, final int parameterCount) {
41+
// JDK explicitly allows NetworkChannel.supportedOptions() post-close
42+
return parameterCount == 0 && name.equals("supportedOptions") && NetworkChannel.class.equals(declaringClass);
43+
}
44+
45+
/**
46+
* Tests whether the given method returns 'this' (the channel) as per JDK spec.
47+
*
48+
* @param declaringClass The class declaring the method.
49+
* @param name The method name.
50+
* @param parameterCount The number of parameters.
51+
* @return {@code true} if the method returns 'this', {@code false} otherwise.
52+
*/
53+
private static boolean returnsThis(final Class<?> declaringClass, final String name, final int parameterCount) {
54+
if (SeekableByteChannel.class.equals(declaringClass)) {
55+
// SeekableByteChannel.position(long) and truncate(long) return 'this'
56+
return parameterCount == 1 && (name.equals("position") || name.equals("truncate"));
57+
}
58+
if (NetworkChannel.class.equals(declaringClass)) {
59+
// NetworkChannel.bind and NetworkChannel.setOption returns 'this'
60+
return parameterCount == 1 && name.equals("bind") || parameterCount == 2 && name.equals("setOption");
61+
}
62+
return false;
63+
}
64+
3165
private final Channel delegate;
3266
private volatile boolean closed;
3367

@@ -40,96 +74,57 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
4074
final Class<?> declaringClass = method.getDeclaringClass();
4175
final String name = method.getName();
4276
final int parameterCount = method.getParameterCount();
43-
4477
// 1) java.lang.Object methods
4578
if (declaringClass == Object.class) {
4679
return invokeObjectMethod(proxy, method, args);
4780
}
48-
4981
// 2) Channel.close(): mark shield closed, do NOT close the delegate
5082
if (parameterCount == 0 && name.equals("close")) {
5183
closed = true;
5284
return null;
5385
}
54-
5586
// 3) Channel.isOpen(): reflect shield state only
5687
if (parameterCount == 0 && name.equals("isOpen")) {
5788
return !closed && delegate.isOpen();
5889
}
59-
6090
// 4) After the shield is closed, only allow a tiny allowlist of safe queries
6191
if (closed && !isAllowedAfterClose(declaringClass, name, parameterCount)) {
6292
throw new ClosedChannelException();
6393
}
64-
6594
// 5) Delegate to the underlying channel and unwrap target exceptions
6695
try {
6796
final Object result = method.invoke(delegate, args);
6897
return returnsThis(declaringClass, name, parameterCount) ? proxy : result;
69-
} catch (InvocationTargetException e) {
98+
} catch (final InvocationTargetException e) {
7099
throw e.getCause();
71100
}
72101
}
73102

74-
/**
75-
* Tests whether the given method is allowed to be called after the shield is closed.
76-
*
77-
* @param declaringClass The class declaring the method.
78-
* @param name The method name.
79-
* @param parameterCount The number of parameters.
80-
* @return {@code true} if the method is allowed after {@code close()}, {@code false} otherwise.
81-
*/
82-
private static boolean isAllowedAfterClose(Class<?> declaringClass, String name, int parameterCount) {
83-
// JDK explicitly allows NetworkChannel.supportedOptions() post-close
84-
return parameterCount == 0 && name.equals("supportedOptions") && NetworkChannel.class.equals(declaringClass);
85-
}
86-
87-
/**
88-
* Tests whether the given method returns 'this' (the channel) as per JDK spec.
89-
*
90-
* @param declaringClass The class declaring the method.
91-
* @param name The method name.
92-
* @param parameterCount The number of parameters.
93-
* @return {@code true} if the method returns 'this', {@code false} otherwise.
94-
*/
95-
private static boolean returnsThis(Class<?> declaringClass, String name, int parameterCount) {
96-
if (SeekableByteChannel.class.equals(declaringClass)) {
97-
// SeekableByteChannel.position(long) and truncate(long) return 'this'
98-
return parameterCount == 1 && (name.equals("position") || name.equals("truncate"));
99-
}
100-
if (NetworkChannel.class.equals(declaringClass)) {
101-
// NetworkChannel.bind and NetworkChannel.setOption returns 'this'
102-
return parameterCount == 1 && name.equals("bind") || parameterCount == 2 && name.equals("setOption");
103-
}
104-
return false;
105-
}
106-
107-
private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args)
108-
throws ReflectiveOperationException {
103+
private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args) {
109104
switch (method.getName()) {
110-
case "toString":
111-
return "CloseShield(" + delegate + ")";
112-
case "hashCode":
113-
return Objects.hashCode(delegate);
114-
case "equals": {
115-
final Object other = args[0];
116-
if (other == null) {
117-
return false;
118-
}
119-
if (proxy == other) {
120-
return true;
121-
}
122-
if (Proxy.isProxyClass(other.getClass())) {
123-
final InvocationHandler h = Proxy.getInvocationHandler(other);
124-
if (h instanceof CloseShieldChannelHandler) {
125-
return Objects.equals(((CloseShieldChannelHandler) h).delegate, this.delegate);
126-
}
127-
}
105+
case "toString":
106+
return "CloseShield(" + delegate + ")";
107+
case "hashCode":
108+
return Objects.hashCode(delegate);
109+
case "equals": {
110+
final Object other = args[0];
111+
if (other == null) {
128112
return false;
129113
}
130-
default:
131-
// Not possible, all non-final Object methods are handled above
132-
return null;
114+
if (proxy == other) {
115+
return true;
116+
}
117+
if (Proxy.isProxyClass(other.getClass())) {
118+
final InvocationHandler h = Proxy.getInvocationHandler(other);
119+
if (h instanceof CloseShieldChannelHandler) {
120+
return Objects.equals(((CloseShieldChannelHandler) h).delegate, this.delegate);
121+
}
122+
}
123+
return false;
124+
}
125+
default:
126+
// Not possible, all non-final Object methods are handled above
127+
return null;
133128
}
134129
}
135130
}

0 commit comments

Comments
 (0)