-
Notifications
You must be signed in to change notification settings - Fork 217
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'opensearch-project:main' into secrets-variable-interface
Showing
31 changed files
with
770 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
...-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/Experimental.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.model.annotations; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Marks a Data Prepper plugin as experimental. | ||
* <p> | ||
* Experimental plugins do not have the same compatibility guarantees as other plugins and may be unstable. | ||
* They may have breaking changes between minor versions and may even be removed. | ||
* <p> | ||
* Data Prepper administrators must enable experimental plugins in order to use them. | ||
* Otherwise, they are not available to use with pipelines. | ||
* | ||
* @since 2.11 | ||
*/ | ||
@Documented | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ElementType.TYPE}) | ||
public @interface Experimental { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
...prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestExperimentalPlugin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugins; | ||
|
||
import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; | ||
import org.opensearch.dataprepper.model.annotations.Experimental; | ||
import org.opensearch.dataprepper.plugin.TestPluggableInterface; | ||
|
||
@DataPrepperPlugin(name = "test_experimental_plugin", pluginType = TestPluggableInterface.class) | ||
@Experimental | ||
public class TestExperimentalPlugin { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
...epper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DefinedPlugin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import java.util.Objects; | ||
|
||
class DefinedPlugin<T> { | ||
private final Class<? extends T> pluginClass; | ||
private final String pluginName; | ||
|
||
public DefinedPlugin(final Class<? extends T> pluginClass, final String pluginName) { | ||
this.pluginClass = Objects.requireNonNull(pluginClass); | ||
this.pluginName = Objects.requireNonNull(pluginName); | ||
} | ||
|
||
public Class<? extends T> getPluginClass() { | ||
return pluginClass; | ||
} | ||
|
||
public String getPluginName() { | ||
return pluginName; | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
...n-framework/src/main/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.inject.Named; | ||
import java.util.function.Consumer; | ||
|
||
@Named | ||
class DeprecatedPluginDetector implements Consumer<DefinedPlugin<?>> { | ||
private static final Logger LOG = LoggerFactory.getLogger(DeprecatedPluginDetector.class); | ||
|
||
@Override | ||
public void accept(final DefinedPlugin<?> definedPlugin) { | ||
logDeprecatedPluginsNames(definedPlugin.getPluginClass(), definedPlugin.getPluginName()); | ||
} | ||
|
||
private void logDeprecatedPluginsNames(final Class<?> pluginClass, final String pluginName) { | ||
final String deprecatedName = pluginClass.getAnnotation(DataPrepperPlugin.class).deprecatedName(); | ||
final String name = pluginClass.getAnnotation(DataPrepperPlugin.class).name(); | ||
if (deprecatedName.equals(pluginName)) { | ||
LOG.warn("Plugin name '{}' is deprecated and will be removed in the next major release. Consider using the updated plugin name '{}'.", deprecatedName, name); | ||
} | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
...-framework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
/** | ||
* Data Prepper configurations for experimental features. | ||
* | ||
* @since 2.11 | ||
*/ | ||
public class ExperimentalConfiguration { | ||
@JsonProperty("enable_all") | ||
private boolean enableAll = false; | ||
|
||
public static ExperimentalConfiguration defaultConfiguration() { | ||
return new ExperimentalConfiguration(); | ||
} | ||
|
||
/** | ||
* Gets whether all experimental features are enabled. | ||
* @return true if all experimental features are enabled, false otherwise | ||
* @since 2.11 | ||
*/ | ||
public boolean isEnableAll() { | ||
return enableAll; | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
...k/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationContainer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
/** | ||
* Interface to decouple how an experimental configuration is defined from | ||
* usage of those configurations. | ||
* | ||
* @since 2.11 | ||
*/ | ||
public interface ExperimentalConfigurationContainer { | ||
/** | ||
* Gets the experimental configuration. | ||
* @return the experimental configuration | ||
* @since 2.11 | ||
*/ | ||
ExperimentalConfiguration getExperimental(); | ||
} |
37 changes: 37 additions & 0 deletions
37
...ramework/src/main/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import org.opensearch.dataprepper.model.annotations.Experimental; | ||
import org.opensearch.dataprepper.model.plugin.NoPluginFoundException; | ||
|
||
import javax.inject.Named; | ||
import java.util.function.Consumer; | ||
|
||
@Named | ||
class ExperimentalPluginValidator implements Consumer<DefinedPlugin<?>> { | ||
private final ExperimentalConfiguration experimentalConfiguration; | ||
|
||
ExperimentalPluginValidator(final ExperimentalConfigurationContainer experimentalConfigurationContainer) { | ||
this.experimentalConfiguration = experimentalConfigurationContainer.getExperimental(); | ||
} | ||
|
||
@Override | ||
public void accept(final DefinedPlugin<?> definedPlugin) { | ||
if(isPluginDisallowedAsExperimental(definedPlugin.getPluginClass())) { | ||
throw new NoPluginFoundException("Unable to create experimental plugin " + definedPlugin.getPluginName() + | ||
". You must enable experimental plugins in data-prepper-config.yaml in order to use them."); | ||
} | ||
} | ||
|
||
private boolean isPluginDisallowedAsExperimental(final Class<?> pluginClass) { | ||
return pluginClass.isAnnotationPresent(Experimental.class) && !experimentalConfiguration.isEnableAll(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
...amework/src/test/java/org/opensearch/dataprepper/plugin/DeprecatedPluginDetectorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import ch.qos.logback.classic.Logger; | ||
import ch.qos.logback.classic.spi.ILoggingEvent; | ||
import ch.qos.logback.core.AppenderBase; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; | ||
import org.opensearch.dataprepper.model.processor.Processor; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.collection.IsEmptyCollection.empty; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class DeprecatedPluginDetectorTest { | ||
@Mock | ||
private DefinedPlugin definedPlugin; | ||
private TestLogAppender testAppender; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
final Logger logger = (Logger) LoggerFactory.getLogger(DeprecatedPluginDetector.class); | ||
|
||
testAppender = new TestLogAppender(); | ||
testAppender.start(); | ||
logger.addAppender(testAppender); | ||
} | ||
|
||
private DeprecatedPluginDetector createObjectUnderTest() { | ||
return new DeprecatedPluginDetector(); | ||
} | ||
|
||
@Test | ||
void accept_on_plugin_without_deprecated_name_does_not_log() { | ||
when(definedPlugin.getPluginClass()).thenReturn(PluginWithoutDeprecatedName.class); | ||
createObjectUnderTest().accept(definedPlugin); | ||
|
||
assertThat(testAppender.getLoggedEvents(), empty()); | ||
} | ||
|
||
@Test | ||
void accept_on_plugin_with_deprecated_name_does_not_log_if_new_name_is_used() { | ||
when(definedPlugin.getPluginClass()).thenReturn(PluginWithDeprecatedName.class); | ||
when(definedPlugin.getPluginName()).thenReturn("test_for_deprecated_detection"); | ||
createObjectUnderTest().accept(definedPlugin); | ||
|
||
assertThat(testAppender.getLoggedEvents(), empty()); | ||
} | ||
|
||
@Test | ||
void accept_on_plugin_with_deprecated_name_logs_if_deprecated_name_is_used() { | ||
when(definedPlugin.getPluginClass()).thenReturn(PluginWithDeprecatedName.class); | ||
when(definedPlugin.getPluginName()).thenReturn("test_for_deprecated_detection_deprecated_name"); | ||
createObjectUnderTest().accept(definedPlugin); | ||
|
||
assertThat(testAppender.getLoggedEvents().stream() | ||
.anyMatch(event -> event.getFormattedMessage().contains("Plugin name 'test_for_deprecated_detection_deprecated_name' is deprecated and will be removed in the next major release. Consider using the updated plugin name 'test_for_deprecated_detection'.")), | ||
equalTo(true)); | ||
} | ||
|
||
@DataPrepperPlugin(name = "test_for_deprecated_detection", pluginType = Processor.class) | ||
private static class PluginWithoutDeprecatedName { | ||
} | ||
|
||
@DataPrepperPlugin(name = "test_for_deprecated_detection", pluginType = Processor.class, deprecatedName = "test_for_deprecated_detection_deprecated_name") | ||
private static class PluginWithDeprecatedName { | ||
} | ||
|
||
public static class TestLogAppender extends AppenderBase<ILoggingEvent> { | ||
private final List<ILoggingEvent> events = new ArrayList<>(); | ||
|
||
@Override | ||
protected void append(final ILoggingEvent eventObject) { | ||
events.add(eventObject); | ||
} | ||
|
||
public List<ILoggingEvent> getLoggedEvents() { | ||
return Collections.unmodifiableList(events); | ||
} | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
...mework/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalConfigurationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.CoreMatchers.notNullValue; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
|
||
class ExperimentalConfigurationTest { | ||
@Test | ||
void defaultConfiguration_should_return_config_with_isEnableAll_false() { | ||
final ExperimentalConfiguration objectUnderTest = ExperimentalConfiguration.defaultConfiguration(); | ||
assertThat(objectUnderTest, notNullValue()); | ||
assertThat(objectUnderTest.isEnableAll(), equalTo(false)); | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
...work/src/test/java/org/opensearch/dataprepper/plugin/ExperimentalPluginValidatorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.dataprepper.plugin; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.opensearch.dataprepper.model.annotations.Experimental; | ||
import org.opensearch.dataprepper.model.plugin.NoPluginFoundException; | ||
|
||
import java.util.UUID; | ||
|
||
import static org.hamcrest.CoreMatchers.containsString; | ||
import static org.hamcrest.CoreMatchers.notNullValue; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class ExperimentalPluginValidatorTest { | ||
|
||
@Mock | ||
private ExperimentalConfigurationContainer experimentalConfigurationContainer; | ||
|
||
@Mock | ||
private ExperimentalConfiguration experimentalConfiguration; | ||
|
||
@Mock | ||
private DefinedPlugin definedPlugin; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
when(experimentalConfigurationContainer.getExperimental()).thenReturn(experimentalConfiguration); | ||
} | ||
|
||
private ExperimentalPluginValidator createObjectUnderTest() { | ||
return new ExperimentalPluginValidator(experimentalConfigurationContainer); | ||
} | ||
|
||
@Test | ||
void accept_with_non_Experimental_plugin_returns() { | ||
when(definedPlugin.getPluginClass()).thenReturn(NonExperimentalPlugin.class); | ||
|
||
createObjectUnderTest().accept(definedPlugin); | ||
} | ||
|
||
@Nested | ||
class WithExperimentalPlugin { | ||
@BeforeEach | ||
void setUp() { | ||
when(definedPlugin.getPluginClass()).thenReturn(ExperimentalPlugin.class); | ||
} | ||
|
||
@Test | ||
void accept_with_Experimental_plugin_throws_if_experimental_is_not_enabled() { | ||
final String pluginName = UUID.randomUUID().toString(); | ||
when(definedPlugin.getPluginName()).thenReturn(pluginName); | ||
|
||
final ExperimentalPluginValidator objectUnderTest = createObjectUnderTest(); | ||
|
||
final NoPluginFoundException actualException = assertThrows(NoPluginFoundException.class, () -> objectUnderTest.accept(definedPlugin)); | ||
|
||
assertThat(actualException.getMessage(), notNullValue()); | ||
assertThat(actualException.getMessage(), containsString(pluginName)); | ||
assertThat(actualException.getMessage(), containsString("experimental plugin")); | ||
} | ||
|
||
@Test | ||
void accept_with_Experimental_plugin_does_not_throw_if_experimental_is_enabled() { | ||
when(experimentalConfiguration.isEnableAll()).thenReturn(true); | ||
|
||
createObjectUnderTest().accept(definedPlugin); | ||
} | ||
} | ||
|
||
private static class NonExperimentalPlugin { | ||
} | ||
|
||
@Experimental | ||
private static class ExperimentalPlugin { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters