Add support for filter chain in Restful module (#1760)
diff --git a/elasticjob-infra/elasticjob-restful/pom.xml b/elasticjob-infra/elasticjob-restful/pom.xml
index 7a7a2a7..3ba7414 100644
--- a/elasticjob-infra/elasticjob-restful/pom.xml
+++ b/elasticjob-infra/elasticjob-restful/pom.xml
@@ -52,6 +52,14 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-inline</artifactId>
+ </dependency>
</dependencies>
<build>
<resources>
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Filter.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Filter.java
new file mode 100644
index 0000000..ed618ce
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Filter.java
@@ -0,0 +1,38 @@
+/*
+ * 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.shardingsphere.elasticjob.restful;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import org.apache.shardingsphere.elasticjob.restful.filter.FilterChain;
+
+/**
+ * HTTP request filter.
+ */
+public interface Filter {
+
+ /**
+ * Do filter.
+ *
+ * @param httpRequest HTTP request
+ * @param httpResponse HTTP response
+ * @param filterChain filter chain
+ * @return pass through the filter if true, else do response
+ */
+ boolean doFilter(FullHttpRequest httpRequest, FullHttpResponse httpResponse, FilterChain filterChain);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
index ec4c68a..9a68787 100644
--- a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
@@ -47,11 +47,22 @@
@Setter
private boolean trailingSlashSensitive;
+ private final List<Filter> filterInstances = new LinkedList<>();
+
private final List<RestfulController> controllerInstances = new LinkedList<>();
private final Map<Class<? extends Throwable>, ExceptionHandler<? extends Throwable>> exceptionHandlers = new HashMap<>();
/**
+ * Add instances of {@link Filter}.
+ *
+ * @param instances instances of Filter
+ */
+ public void addFilterInstances(final Filter... instances) {
+ filterInstances.addAll(Arrays.asList(instances));
+ }
+
+ /**
* Add instances of RestfulController.
*
* @param instances instances of RestfulController
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChain.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChain.java
new file mode 100644
index 0000000..927ca4c
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChain.java
@@ -0,0 +1,73 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.filter;
+
+import com.google.common.base.Preconditions;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.util.ReferenceCountUtil;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+
+import java.util.List;
+
+/**
+ * Default filter chain.
+ */
+public final class DefaultFilterChain implements FilterChain {
+
+ private final Filter[] filters;
+
+ private final ChannelHandlerContext ctx;
+
+ private final HandleContext<?> handleContext;
+
+ private int current;
+
+ private boolean finished;
+
+ public DefaultFilterChain(final List<Filter> filterInstances, final ChannelHandlerContext ctx, final HandleContext<?> handleContext) {
+ filters = filterInstances.toArray(new Filter[0]);
+ this.ctx = ctx;
+ this.handleContext = handleContext;
+ }
+
+ @Override
+ public void next(final FullHttpRequest httpRequest) {
+ Preconditions.checkState(!finished, "FilterChain has already finished.");
+ if (current < filters.length) {
+ Filter currentFilter = filters[current++];
+ boolean passThrough = currentFilter.doFilter(httpRequest, handleContext.getHttpResponse(), this);
+ if (!passThrough) {
+ finished = true;
+ doResponse();
+ }
+ return;
+ }
+ finished = true;
+ ctx.fireChannelRead(handleContext);
+ }
+
+ private void doResponse() {
+ try {
+ ctx.writeAndFlush(handleContext.getHttpResponse());
+ } finally {
+ ReferenceCountUtil.release(handleContext.getHttpRequest());
+ }
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/FilterChain.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/FilterChain.java
new file mode 100644
index 0000000..1d46680
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/FilterChain.java
@@ -0,0 +1,33 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.filter;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+
+/**
+ * Filter chain for {@link org.apache.shardingsphere.elasticjob.restful.Filter}.
+ */
+public interface FilterChain {
+
+ /**
+ * Next filter.
+ *
+ * @param httpRequest HTTP request
+ */
+ void next(FullHttpRequest httpRequest);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandler.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandler.java
new file mode 100644
index 0000000..7247dd1
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.filter.DefaultFilterChain;
+import org.apache.shardingsphere.elasticjob.restful.filter.FilterChain;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+
+import java.util.List;
+
+/**
+ * Filter chain inbound handler.
+ */
+@Slf4j
+@Sharable
+@RequiredArgsConstructor
+public final class FilterChainInboundHandler extends ChannelInboundHandlerAdapter {
+
+ private final List<Filter> filterInstances;
+
+ @SuppressWarnings({"NullableProblems", "unchecked"})
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
+ if (filterInstances.isEmpty()) {
+ ctx.fireChannelRead(msg);
+ return;
+ }
+ HandleContext<Handler> handleContext = (HandleContext<Handler>) msg;
+ FilterChain filterChain = new DefaultFilterChain(filterInstances, ctx, handleContext);
+ filterChain.next(handleContext.getHttpRequest());
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
index b0bd744..06070c5 100644
--- a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
@@ -31,6 +31,8 @@
private final ContextInitializationInboundHandler contextInitializationInboundHandler;
+ private final FilterChainInboundHandler filterChainInboundHandler;
+
private final HttpRequestDispatcher httpRequestDispatcher;
private final HandlerParameterDecoder handlerParameterDecoder;
@@ -41,6 +43,7 @@
public RestfulServiceChannelInitializer(final NettyRestfulServiceConfiguration configuration) {
contextInitializationInboundHandler = new ContextInitializationInboundHandler();
+ filterChainInboundHandler = new FilterChainInboundHandler(configuration.getFilterInstances());
httpRequestDispatcher = new HttpRequestDispatcher(configuration.getControllerInstances(), configuration.isTrailingSlashSensitive());
handlerParameterDecoder = new HandlerParameterDecoder();
handleMethodExecutor = new HandleMethodExecutor();
@@ -53,6 +56,7 @@
pipeline.addLast("codec", new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(1024 * 1024));
pipeline.addLast("contextInitialization", contextInitializationInboundHandler);
+ pipeline.addLast("filterChain", filterChainInboundHandler);
pipeline.addLast("dispatcher", httpRequestDispatcher);
pipeline.addLast("handlerParameterDecoder", handlerParameterDecoder);
pipeline.addLast("handleMethodExecutor", handleMethodExecutor);
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChainTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChainTest.java
new file mode 100644
index 0000000..067fdd1
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChainTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.filter;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import lombok.SneakyThrows;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class DefaultFilterChainTest {
+
+ @Mock
+ private ChannelHandlerContext ctx;
+
+ @Mock
+ private FullHttpRequest httpRequest;
+
+ @Mock
+ private FullHttpResponse httpResponse;
+
+ @Mock
+ private Filter firstFilter;
+
+ @Mock
+ private Filter secondFilter;
+
+ @Mock
+ private Filter thirdFilter;
+
+ private HandleContext<Handler> handleContext;
+
+ @Before
+ public void setUp() {
+ handleContext = new HandleContext<>(httpRequest, httpResponse);
+ }
+
+ @Test
+ public void assertNoFilter() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Collections.emptyList(), ctx, handleContext);
+ filterChain.next(httpRequest);
+ verify(ctx, never()).writeAndFlush(httpResponse);
+ verify(ctx).fireChannelRead(handleContext);
+ }
+
+ @Test
+ public void assertWithSingleFilterPassed() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Collections.singletonList(firstFilter), ctx, handleContext);
+ when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+ filterChain.next(httpRequest);
+ verify(ctx).fireChannelRead(handleContext);
+ verify(ctx, never()).writeAndFlush(httpResponse);
+ }
+
+ @Test
+ public void assertWithSingleFilterDoResponse() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Collections.singletonList(firstFilter), ctx, handleContext);
+ filterChain.next(httpRequest);
+ verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+ verify(ctx, never()).fireChannelRead(any(HandleContext.class));
+ verify(ctx).writeAndFlush(httpResponse);
+ }
+
+ @Test
+ public void assertWithThreeFiltersPassed() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Arrays.asList(firstFilter, secondFilter, thirdFilter), ctx, handleContext);
+ when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+ when(secondFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(secondFilter).doFilter(httpRequest, httpResponse, filterChain);
+ when(thirdFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(thirdFilter).doFilter(httpRequest, httpResponse, filterChain);
+ filterChain.next(httpRequest);
+ verify(ctx).fireChannelRead(handleContext);
+ verify(ctx, never()).writeAndFlush(any(FullHttpResponse.class));
+ }
+
+ @Test
+ public void assertWithThreeFiltersDoResponseByTheSecond() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Arrays.asList(firstFilter, secondFilter, thirdFilter), ctx, handleContext);
+ when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+ when(secondFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(false);
+ assertFalse(isFinished(filterChain));
+ filterChain.next(httpRequest);
+ verify(secondFilter).doFilter(httpRequest, httpResponse, filterChain);
+ assertTrue(isFinished(filterChain));
+ verify(thirdFilter, never()).doFilter(httpRequest, httpResponse, filterChain);
+ verify(ctx, never()).fireChannelRead(any(HandleContext.class));
+ verify(ctx).writeAndFlush(httpResponse);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void assertInvokeFinishedFilterChainWithoutFilter() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Collections.emptyList(), ctx, handleContext);
+ filterChain.next(httpRequest);
+ filterChain.next(httpRequest);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void assertInvokeFinishedFilterChainWithTwoFilters() {
+ DefaultFilterChain filterChain = new DefaultFilterChain(Arrays.asList(firstFilter, secondFilter), ctx, handleContext);
+ when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+ when(secondFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+ filterChain.next(httpRequest);
+ verify(secondFilter).doFilter(httpRequest, httpResponse, filterChain);
+ filterChain.next(httpRequest);
+ verify(ctx).fireChannelRead(handleContext);
+ filterChain.next(httpRequest);
+ }
+
+ @SneakyThrows
+ private boolean isFinished(final DefaultFilterChain filterChain) {
+ Field field = DefaultFilterChain.class.getDeclaredField("finished");
+ field.setAccessible(true);
+ return (boolean) field.get(filterChain);
+ }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandlerTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandlerTest.java
new file mode 100644
index 0000000..d25e444
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandlerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+import lombok.SneakyThrows;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class FilterChainInboundHandlerTest {
+
+ @Mock
+ private List<Filter> filterInstances;
+
+ @Mock
+ private HandleContext<Handler> handleContext;
+
+ private EmbeddedChannel channel;
+
+ @Before
+ public void setUp() {
+ channel = new EmbeddedChannel(new FilterChainInboundHandler(filterInstances));
+ }
+
+ @Test
+ @SneakyThrows
+ public void assertNoFilter() {
+ when(filterInstances.isEmpty()).thenReturn(true);
+ channel.writeOneInbound(handleContext);
+ verify(handleContext, never()).getHttpRequest();
+ }
+
+ @Test
+ public void assertFilterExists() {
+ when(filterInstances.isEmpty()).thenReturn(false);
+ channel.writeOneInbound(handleContext);
+ verify(handleContext, atLeastOnce()).getHttpRequest();
+ }
+}