You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by ce...@apache.org on 2016/12/24 01:41:06 UTC
incubator-metron git commit: METRON-639: The Network Stellar
functions need to have better unit testing closes apache/incubator-metron#402
Repository: incubator-metron
Updated Branches:
refs/heads/master accfce8f9 -> f3376755c
METRON-639: The Network Stellar functions need to have better unit testing closes apache/incubator-metron#402
Project: http://git-wip-us.apache.org/repos/asf/incubator-metron/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-metron/commit/f3376755
Tree: http://git-wip-us.apache.org/repos/asf/incubator-metron/tree/f3376755
Diff: http://git-wip-us.apache.org/repos/asf/incubator-metron/diff/f3376755
Branch: refs/heads/master
Commit: f3376755c9df896ed5b0082faad4712d1e99b1ee
Parents: accfce8
Author: cstella <ce...@gmail.com>
Authored: Fri Dec 23 20:40:32 2016 -0500
Committer: cstella <ce...@gmail.com>
Committed: Fri Dec 23 20:40:32 2016 -0500
----------------------------------------------------------------------
.../common/dsl/functions/NetworkFunctions.java | 81 +++++++---
.../stellar/network/NetworkFunctionsTest.java | 155 +++++++++++++++++++
.../stellar/network/RemoveSubdomainsTest.java | 48 ------
.../common/utils/StellarProcessorUtils.java | 148 ++++++++++++++----
4 files changed, 330 insertions(+), 102 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/f3376755/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/NetworkFunctions.java
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/NetworkFunctions.java b/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/NetworkFunctions.java
index f33a4a5..2dc92c9 100644
--- a/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/NetworkFunctions.java
+++ b/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/NetworkFunctions.java
@@ -18,10 +18,10 @@
package org.apache.metron.common.dsl.functions;
-import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.net.InternetDomainName;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.util.SubnetUtils;
import org.apache.metron.common.dsl.BaseStellarFunction;
import org.apache.metron.common.dsl.Stellar;
@@ -29,7 +29,6 @@ import org.apache.metron.common.dsl.Stellar;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
-import java.util.function.Function;
public class NetworkFunctions {
@Stellar(name="IN_SUBNET"
@@ -55,7 +54,7 @@ public class NetworkFunctions {
}
boolean inSubnet = false;
for(int i = 1;i < list.size() && !inSubnet;++i) {
- String cidr = (String) list.get(1);
+ String cidr = (String) list.get(i);
if(cidr == null) {
continue;
}
@@ -86,15 +85,19 @@ public class NetworkFunctions {
InternetDomainName idn = toDomainName(dnObj);
if(idn != null) {
String dn = dnObj.toString();
- String tld = Joiner.on(".").join(idn.publicSuffix().parts());
- String suffix = dn.substring(0, dn.length() - tld.length());
- String hostnameWithoutTLD = suffix.substring(0, suffix.length() - 1);
- String hostnameWithoutSubsAndTLD = Iterables.getLast(Splitter.on(".").split(hostnameWithoutTLD), null);
- if(hostnameWithoutSubsAndTLD == null) {
- return null;
+ String tld = extractTld(idn, dn);
+ if(!StringUtils.isEmpty(dn)) {
+ String suffix = safeSubstring(dn, 0, dn.length() - tld.length());
+ String hostnameWithoutTLD = safeSubstring(suffix, 0, suffix.length() - 1);
+ if(hostnameWithoutTLD == null) {
+ return dn;
+ }
+ String hostnameWithoutSubsAndTLD = Iterables.getLast(Splitter.on(".").split(hostnameWithoutTLD), null);
+ if(hostnameWithoutSubsAndTLD == null) {
+ return null;
+ }
+ return hostnameWithoutSubsAndTLD + "." + tld;
}
- return hostnameWithoutSubsAndTLD + "." + tld;
-
}
return null;
}
@@ -116,14 +119,13 @@ public class NetworkFunctions {
InternetDomainName idn = toDomainName(dnObj);
if(idn != null) {
String dn = dnObj.toString();
- String tld = idn.publicSuffix().toString();
- String suffix = Iterables.getFirst(Splitter.on(tld).split(dn), null);
- if(suffix != null)
- {
- return suffix.substring(0, suffix.length() - 1);
+ String tld = extractTld(idn, dn);
+ String suffix = safeSubstring(dn, 0, dn.length() - tld.length());
+ if(StringUtils.isEmpty(suffix)) {
+ return suffix;
}
else {
- return null;
+ return suffix.substring(0, suffix.length() - 1);
}
}
return null;
@@ -144,10 +146,7 @@ public class NetworkFunctions {
public Object apply(List<Object> objects) {
Object dnObj = objects.get(0);
InternetDomainName idn = toDomainName(dnObj);
- if(idn != null) {
- return idn.publicSuffix().toString();
- }
- return null;
+ return extractTld(idn, dnObj + "");
}
}
@@ -220,6 +219,43 @@ public class NetworkFunctions {
}
}
+ /**
+ * Extract the TLD. If the domain is a normal domain, then we can handle the TLD via the InternetDomainName object.
+ * If it is not, then we default to returning the last segment after the final '.'
+ * @param idn
+ * @param dn
+ * @return The TLD of the domain
+ */
+ private static String extractTld(InternetDomainName idn, String dn) {
+
+ if(idn != null && idn.hasPublicSuffix()) {
+ return idn.publicSuffix().toString();
+ }
+ else if(dn != null) {
+ StringBuffer tld = new StringBuffer("");
+ for(int idx = dn.length() -1;idx >= 0;idx--) {
+ char c = dn.charAt(idx);
+ if(c == '.') {
+ break;
+ }
+ else {
+ tld.append(dn.charAt(idx));
+ }
+ }
+ return tld.reverse().toString();
+ }
+ else {
+ return null;
+ }
+ }
+
+ private static String safeSubstring(String val, int start, int end) {
+ if(!StringUtils.isEmpty(val)) {
+ return val.substring(start, end);
+ }
+ return null;
+ }
+
private static InternetDomainName toDomainName(Object dnObj) {
if(dnObj != null) {
if(dnObj instanceof String) {
@@ -243,8 +279,9 @@ public class NetworkFunctions {
return null;
}
if(urlObj instanceof String) {
+ String url = urlObj.toString();
try {
- return new URL(urlObj.toString());
+ return new URL(url);
} catch (MalformedURLException e) {
return null;
}
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/f3376755/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/NetworkFunctionsTest.java
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/NetworkFunctionsTest.java b/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/NetworkFunctionsTest.java
new file mode 100644
index 0000000..783658c
--- /dev/null
+++ b/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/NetworkFunctionsTest.java
@@ -0,0 +1,155 @@
+/**
+ * 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.metron.common.stellar.network;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+
+import static org.apache.metron.common.utils.StellarProcessorUtils.runWithArguments;
+
+public class NetworkFunctionsTest {
+
+ @Test
+ public void inSubnetTest_positive() {
+ runWithArguments("IN_SUBNET", ImmutableList.of("192.168.0.1", "192.168.0.0/24"), true);
+ }
+
+ @Test
+ public void inSubnetTest_negative() {
+ runWithArguments("IN_SUBNET", ImmutableList.of("192.168.1.1", "192.168.0.0/24"), false);
+ }
+
+ @Test
+ public void inSubnetTest_multiple() {
+ runWithArguments("IN_SUBNET", ImmutableList.of("192.168.1.1", "192.168.0.0/24", "192.168.1.0/24"), true);
+ }
+
+ @Test
+ public void removeSubdomainsTest() {
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "www.google.co.uk", "google.co.uk");
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "www.google.com", "google.com");
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "com", "com");
+ }
+
+ @Test
+ public void removeSubdomainsTest_tld_square() {
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "com.com", "com.com");
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "net.net", "net.net");
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "co.uk.co.uk", "uk.co.uk");
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "www.subdomain.com.com", "com.com");
+ }
+
+ @Test
+ public void removeSubdomainsTest_unknowntld() {
+ runWithArguments("DOMAIN_REMOVE_SUBDOMAINS", "www.subdomain.google.gmail", "google.gmail");
+ }
+
+ @Test
+ public void toTldTest() {
+ runWithArguments("DOMAIN_TO_TLD", "www.google.co.uk", "co.uk");
+ runWithArguments("DOMAIN_TO_TLD", "www.google.com", "com");
+ runWithArguments("DOMAIN_TO_TLD", "com", "com");
+ }
+
+ @Test
+ public void toTldTest_tld_square() {
+ runWithArguments("DOMAIN_TO_TLD", "com.com", "com");
+ runWithArguments("DOMAIN_TO_TLD", "net.net", "net");
+ runWithArguments("DOMAIN_TO_TLD", "co.uk.co.uk", "co.uk");
+ runWithArguments("DOMAIN_TO_TLD", "www.subdomain.com.com", "com");
+ }
+
+ @Test
+ public void toTldTest_unknowntld() {
+ runWithArguments("DOMAIN_TO_TLD", "www.subdomain.google.gmail", "gmail");
+ }
+
+ @Test
+ public void removeTldTest() {
+ runWithArguments("DOMAIN_REMOVE_TLD", "www.google.co.uk", "www.google");
+ runWithArguments("DOMAIN_REMOVE_TLD", "www.google.com", "www.google");
+ runWithArguments("DOMAIN_REMOVE_TLD", "com", "");
+ }
+
+ @Test
+ public void removeTldTest_tld_square() {
+ runWithArguments("DOMAIN_REMOVE_TLD", "com.com", "com");
+ runWithArguments("DOMAIN_REMOVE_TLD", "net.net", "net");
+ runWithArguments("DOMAIN_REMOVE_TLD", "co.uk.co.uk", "co.uk");
+ runWithArguments("DOMAIN_REMOVE_TLD", "www.subdomain.com.com", "www.subdomain.com");
+ }
+
+ @Test
+ public void removeTldTest_unknowntld() {
+ runWithArguments("DOMAIN_REMOVE_TLD", "www.subdomain.google.gmail", "www.subdomain.google");
+ }
+
+ @Test
+ public void urlToPortTest() {
+ runWithArguments("URL_TO_PORT", "http://www.google.com/foo/bar", 80);
+ runWithArguments("URL_TO_PORT", "https://www.google.com/foo/bar", 443);
+ runWithArguments("URL_TO_PORT", "http://www.google.com:7979/foo/bar", 7979);
+ }
+
+
+ @Test
+ public void urlToPortTest_unknowntld() {
+ runWithArguments("URL_TO_PORT", "http://www.google.gmail/foo/bar", 80);
+ }
+
+ @Test
+ public void urlToHostTest() {
+ runWithArguments("URL_TO_HOST", "http://www.google.com/foo/bar", "www.google.com");
+ runWithArguments("URL_TO_HOST", "https://www.google.com/foo/bar", "www.google.com");
+ runWithArguments("URL_TO_HOST", "http://www.google.com:7979/foo/bar", "www.google.com");
+ runWithArguments("URL_TO_HOST", "http://localhost:8080/a", "localhost");
+ }
+
+
+ @Test
+ public void urlToHostTest_unknowntld() {
+ runWithArguments("URL_TO_HOST", "http://www.google.gmail/foo/bar", "www.google.gmail");
+ }
+
+ @Test
+ public void urlToProtocolTest() {
+ runWithArguments("URL_TO_PROTOCOL", "http://www.google.com/foo/bar", "http");
+ runWithArguments("URL_TO_PROTOCOL", "https://www.google.com/foo/bar", "https");
+ }
+
+
+ @Test
+ public void urlToProtocolTest_unknowntld() {
+ runWithArguments("URL_TO_PROTOCOL", "http://www.google.gmail/foo/bar", "http");
+ }
+
+ @Test
+ public void urlToPathTest() {
+ runWithArguments("URL_TO_PATH", "http://www.google.com/foo/bar", "/foo/bar");
+ runWithArguments("URL_TO_PATH", "https://www.google.com/foo/bar", "/foo/bar");
+ }
+
+
+ @Test
+ public void urlToPathTest_unknowntld() {
+ runWithArguments("URL_TO_PATH", "http://www.google.gmail/foo/bar", "/foo/bar");
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/f3376755/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/RemoveSubdomainsTest.java
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/RemoveSubdomainsTest.java b/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/RemoveSubdomainsTest.java
deleted file mode 100644
index ab503e9..0000000
--- a/metron-platform/metron-common/src/test/java/org/apache/metron/common/stellar/network/RemoveSubdomainsTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * 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.metron.common.stellar.network;
-
-import com.google.common.collect.ImmutableList;
-import org.apache.metron.common.dsl.functions.NetworkFunctions;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class RemoveSubdomainsTest {
-
- @Test
- public void testEdgeCasesTldSquared() {
- NetworkFunctions.RemoveSubdomains removeSubdomains = new NetworkFunctions.RemoveSubdomains();
- Assert.assertEquals("com.com", removeSubdomains.apply(ImmutableList.of("com.com")));
- Assert.assertEquals("net.net", removeSubdomains.apply(ImmutableList.of("net.net")));
- Assert.assertEquals("uk.co.uk", removeSubdomains.apply(ImmutableList.of("co.uk.co.uk")));
- Assert.assertEquals("com.com", removeSubdomains.apply(ImmutableList.of("www.subdomain.com.com")));
- }
-
- @Test
- public void testHappyPath() {
- NetworkFunctions.RemoveSubdomains removeSubdomains = new NetworkFunctions.RemoveSubdomains();
- Assert.assertEquals("example.com", removeSubdomains.apply(ImmutableList.of("my.example.com")));
- Assert.assertEquals("example.co.uk", removeSubdomains.apply(ImmutableList.of("my.example.co.uk")));
- }
-
- @Test
- public void testEmptyString() {
- NetworkFunctions.RemoveSubdomains removeSubdomains = new NetworkFunctions.RemoveSubdomains();
- Assert.assertEquals(null, removeSubdomains.apply(ImmutableList.of("")));
- }
-}
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/f3376755/metron-platform/metron-common/src/test/java/org/apache/metron/common/utils/StellarProcessorUtils.java
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/src/test/java/org/apache/metron/common/utils/StellarProcessorUtils.java b/metron-platform/metron-common/src/test/java/org/apache/metron/common/utils/StellarProcessorUtils.java
index e7a58bf..7ef84ea 100644
--- a/metron-platform/metron-common/src/test/java/org/apache/metron/common/utils/StellarProcessorUtils.java
+++ b/metron-platform/metron-common/src/test/java/org/apache/metron/common/utils/StellarProcessorUtils.java
@@ -18,55 +18,139 @@
package org.apache.metron.common.utils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
import org.apache.metron.common.dsl.*;
import org.apache.metron.common.stellar.StellarPredicateProcessor;
import org.apache.metron.common.stellar.StellarProcessor;
import org.junit.Assert;
import org.junit.Test;
-import java.util.Map;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
public class StellarProcessorUtils {
- /**
- * This ensures the basic contract of a stellar expression is adhered to:
- * 1. Validate works on the expression
- * 2. The output can be serialized and deserialized properly
- *
- * @param rule
- * @param variables
- * @param context
- * @return
- */
- public static Object run(String rule, Map<String, Object> variables, Context context) {
- StellarProcessor processor = new StellarProcessor();
- Assert.assertTrue(rule + " not valid.", processor.validate(rule, context));
- Object ret = processor.parse(rule, x -> variables.get(x), StellarFunctions.FUNCTION_RESOLVER(), context);
- byte[] raw = SerDeUtils.toBytes(ret);
- Object actual = SerDeUtils.fromBytes(raw, Object.class);
- Assert.assertEquals(ret, actual);
- return ret;
- }
+ /**
+ * This ensures the basic contract of a stellar expression is adhered to:
+ * 1. Validate works on the expression
+ * 2. The output can be serialized and deserialized properly
+ *
+ * @param rule
+ * @param variables
+ * @param context
+ * @return
+ */
+ public static Object run(String rule, Map<String, Object> variables, Context context) {
+ StellarProcessor processor = new StellarProcessor();
+ Assert.assertTrue(rule + " not valid.", processor.validate(rule, context));
+ Object ret = processor.parse(rule, x -> variables.get(x), StellarFunctions.FUNCTION_RESOLVER(), context);
+ byte[] raw = SerDeUtils.toBytes(ret);
+ Object actual = SerDeUtils.fromBytes(raw, Object.class);
+ Assert.assertEquals(ret, actual);
+ return ret;
+ }
+
+ public static Object run(String rule, Map<String, Object> variables) {
+ return run(rule, variables, Context.EMPTY_CONTEXT());
+ }
+
+ public static boolean runPredicate(String rule, Map resolver) {
+ return runPredicate(rule, resolver, Context.EMPTY_CONTEXT());
+ }
+
+ public static boolean runPredicate(String rule, Map resolver, Context context) {
+ return runPredicate(rule, new MapVariableResolver(resolver), context);
+ }
+
+ public static boolean runPredicate(String rule, VariableResolver resolver) {
+ return runPredicate(rule, resolver, Context.EMPTY_CONTEXT());
+ }
+
+ public static boolean runPredicate(String rule, VariableResolver resolver, Context context) {
+ StellarPredicateProcessor processor = new StellarPredicateProcessor();
+ Assert.assertTrue(rule + " not valid.", processor.validate(rule));
+ return processor.parse(rule, resolver, StellarFunctions.FUNCTION_RESOLVER(), context);
+ }
- public static Object run(String rule, Map<String, Object> variables) {
- return run(rule, variables, Context.EMPTY_CONTEXT());
+ public static void runWithArguments(String function, Object argument, Object expected) {
+ runWithArguments(function, ImmutableList.of(argument), expected);
+ }
+
+ public static void runWithArguments(String function, List<Object> arguments, Object expected) {
+ Supplier<Stream<Map.Entry<String, Object>>> kvStream = () -> StreamSupport.stream(new XRange(arguments.size()), false)
+ .map( i -> new AbstractMap.SimpleImmutableEntry<>("var" + i, arguments.get(i)));
+
+ String args = kvStream.get().map( kv -> kv.getKey())
+ .collect(Collectors.joining(","));
+ Map<String, Object> variables = kvStream.get().collect(Collectors.toMap(kv -> kv.getKey(), kv -> kv.getValue()));
+ String stellarStatement = function + "(" + args + ")";
+ String reason = stellarStatement + " != " + expected + " with variables: " + variables;
+
+ if(expected instanceof Double) {
+ Assert.assertEquals(reason, (Double)expected, (Double)run(stellarStatement, variables), 1e-6);
}
+ else {
+ Assert.assertEquals(reason, expected, run(stellarStatement, variables));
+ }
+ }
+
+ public static class XRange extends Spliterators.AbstractIntSpliterator {
+ int end;
+ int i = 0;
- public static boolean runPredicate(String rule, Map resolver) {
- return runPredicate(rule, resolver, Context.EMPTY_CONTEXT());
+ public XRange(int start, int end) {
+ super(end - start, 0);
+ i = start;
+ this.end = end;
}
- public static boolean runPredicate(String rule, Map resolver, Context context) {
- return runPredicate(rule, new MapVariableResolver(resolver), context);
+ public XRange(int end) {
+ this(0, end);
}
- public static boolean runPredicate(String rule, VariableResolver resolver) {
- return runPredicate(rule, resolver, Context.EMPTY_CONTEXT());
+ @Override
+ public boolean tryAdvance(IntConsumer action) {
+ boolean isDone = i >= end;
+ if(isDone) {
+ return false;
+ }
+ else {
+ action.accept(i);
+ i++;
+ return true;
+ }
}
- public static boolean runPredicate(String rule, VariableResolver resolver, Context context) {
- StellarPredicateProcessor processor = new StellarPredicateProcessor();
- Assert.assertTrue(rule + " not valid.", processor.validate(rule));
- return processor.parse(rule, resolver, StellarFunctions.FUNCTION_RESOLVER(), context);
+ /**
+ * {@inheritDoc}
+ *
+ * @param action
+ * @implSpec If the action is an instance of {@code IntConsumer} then it is cast
+ * to {@code IntConsumer} and passed to
+ * {@link #tryAdvance(IntConsumer)}; otherwise
+ * the action is adapted to an instance of {@code IntConsumer}, by
+ * boxing the argument of {@code IntConsumer}, and then passed to
+ * {@link #tryAdvance(IntConsumer)}.
+ */
+ @Override
+ public boolean tryAdvance(Consumer<? super Integer> action) {
+ boolean isDone = i >= end;
+ if(isDone) {
+ return false;
+ }
+ else {
+ action.accept(i);
+ i++;
+ return true;
+ }
}
+ }
+
}