Greg Harris
1 year ago
committed by
GitHub
21 changed files with 1576 additions and 32 deletions
@ -0,0 +1,21 @@ |
|||||||
|
#!/bin/bash |
||||||
|
# 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. |
||||||
|
|
||||||
|
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then |
||||||
|
export KAFKA_HEAP_OPTS="-Xms256M -Xmx2G" |
||||||
|
fi |
||||||
|
|
||||||
|
exec $(dirname $0)/kafka-run-class.sh org.apache.kafka.tools.ConnectPluginPath "$@" |
@ -0,0 +1,21 @@ |
|||||||
|
@echo off |
||||||
|
rem Licensed to the Apache Software Foundation (ASF) under one or more |
||||||
|
rem contributor license agreements. See the NOTICE file distributed with |
||||||
|
rem this work for additional information regarding copyright ownership. |
||||||
|
rem The ASF licenses this file to You under the Apache License, Version 2.0 |
||||||
|
rem (the "License"); you may not use this file except in compliance with |
||||||
|
rem the License. You may obtain a copy of the License at |
||||||
|
rem |
||||||
|
rem http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
rem |
||||||
|
rem Unless required by applicable law or agreed to in writing, software |
||||||
|
rem distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
rem See the License for the specific language governing permissions and |
||||||
|
rem limitations under the License. |
||||||
|
|
||||||
|
IF ["%KAFKA_HEAP_OPTS%"] EQU [""] ( |
||||||
|
set KAFKA_HEAP_OPTS=-Xms256M -Xmx2G |
||||||
|
) |
||||||
|
|
||||||
|
"%~dp0kafka-run-class.bat" org.apache.kafka.tools.ConnectPluginPath %* |
@ -0,0 +1,16 @@ |
|||||||
|
# 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. |
||||||
|
|
||||||
|
test.plugins.NonMigratedMultiPlugin |
@ -0,0 +1,46 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
import org.apache.kafka.connect.data.Schema; |
||||||
|
import org.apache.kafka.connect.data.SchemaAndValue; |
||||||
|
import org.apache.kafka.connect.storage.Converter; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public final class NonMigratedConverter implements Converter { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configure(final Map<String, ?> configs, final boolean isKey) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public byte[] fromConnectData(final String topic, final Schema schema, final Object value) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SchemaAndValue toConnectData(final String topic, final byte[] value) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
import org.apache.kafka.common.config.ConfigDef; |
||||||
|
import org.apache.kafka.connect.data.Schema; |
||||||
|
import org.apache.kafka.connect.data.SchemaAndValue; |
||||||
|
import org.apache.kafka.connect.storage.HeaderConverter; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public class NonMigratedHeaderConverter implements HeaderConverter { |
||||||
|
|
||||||
|
@Override |
||||||
|
public SchemaAndValue toConnectHeader(String topic, String headerKey, byte[] value) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public byte[] fromConnectHeader(String topic, String headerKey, Schema schema, Object value) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConfigDef config() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() throws IOException { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configure(Map<String, ?> configs) { |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
import org.apache.kafka.common.config.ConfigDef; |
||||||
|
import org.apache.kafka.common.config.ConfigValue; |
||||||
|
import org.apache.kafka.connect.connector.ConnectRecord; |
||||||
|
import org.apache.kafka.connect.connector.policy.ConnectorClientConfigOverridePolicy; |
||||||
|
import org.apache.kafka.connect.connector.policy.ConnectorClientConfigRequest; |
||||||
|
import org.apache.kafka.connect.data.Schema; |
||||||
|
import org.apache.kafka.connect.data.SchemaAndValue; |
||||||
|
import org.apache.kafka.connect.storage.Converter; |
||||||
|
import org.apache.kafka.connect.storage.HeaderConverter; |
||||||
|
import org.apache.kafka.connect.transforms.predicates.Predicate; |
||||||
|
import org.apache.kafka.connect.transforms.Transformation; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public final class NonMigratedMultiPlugin implements Converter, HeaderConverter, Predicate, Transformation, ConnectorClientConfigOverridePolicy { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configure(Map<String, ?> configs, boolean isKey) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public byte[] fromConnectData(String topic, Schema schema, Object value) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SchemaAndValue toConnectData(String topic, byte[] value) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SchemaAndValue toConnectHeader(String topic, String headerKey, byte[] value) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public byte[] fromConnectHeader(String topic, String headerKey, Schema schema, Object value) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConnectRecord apply(ConnectRecord record) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConfigDef config() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean test(ConnectRecord record) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configure(Map<String, ?> configs) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<ConfigValue> validate(ConnectorClientConfigRequest connectorClientConfigRequest) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
|
||||||
|
import org.apache.kafka.common.config.ConfigDef; |
||||||
|
import org.apache.kafka.connect.connector.ConnectRecord; |
||||||
|
import org.apache.kafka.connect.transforms.predicates.Predicate; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public class NonMigratedPredicate implements Predicate { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configure(Map<String, ?> configs) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConfigDef config() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean test(ConnectRecord record) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
import org.apache.kafka.common.config.ConfigDef; |
||||||
|
import org.apache.kafka.connect.connector.Task; |
||||||
|
import org.apache.kafka.connect.sink.SinkConnector; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public class NonMigratedSinkConnector extends SinkConnector { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void start(Map<String, String> props) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Class<? extends Task> taskClass() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Map<String, String>> taskConfigs(int maxTasks) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void stop() { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConfigDef config() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String version() { |
||||||
|
return "1.0.0"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
import org.apache.kafka.common.config.ConfigDef; |
||||||
|
import org.apache.kafka.connect.connector.Task; |
||||||
|
import org.apache.kafka.connect.source.SourceConnector; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public class NonMigratedSourceConnector extends SourceConnector { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void start(Map<String, String> props) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Class<? extends Task> taskClass() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Map<String, String>> taskConfigs(int maxTasks) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void stop() { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConfigDef config() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String version() { |
||||||
|
return "1.0.0"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
/* |
||||||
|
* 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 test.plugins; |
||||||
|
|
||||||
|
import org.apache.kafka.common.config.ConfigDef; |
||||||
|
import org.apache.kafka.connect.connector.ConnectRecord; |
||||||
|
import org.apache.kafka.connect.transforms.Transformation; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fake plugin class for testing classloading isolation. |
||||||
|
* See {@link org.apache.kafka.connect.runtime.isolation.TestPlugins}. |
||||||
|
* <p>Class which is not migrated to include a service loader manifest. |
||||||
|
*/ |
||||||
|
public class NonMigratedTransformation implements Transformation { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configure(Map<String, ?> configs) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConnectRecord apply(ConnectRecord record) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ConfigDef config() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,492 @@ |
|||||||
|
/* |
||||||
|
* 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.kafka.tools; |
||||||
|
|
||||||
|
import net.sourceforge.argparse4j.ArgumentParsers; |
||||||
|
import net.sourceforge.argparse4j.impl.Arguments; |
||||||
|
import net.sourceforge.argparse4j.inf.ArgumentGroup; |
||||||
|
import net.sourceforge.argparse4j.inf.ArgumentParser; |
||||||
|
import net.sourceforge.argparse4j.inf.ArgumentParserException; |
||||||
|
import net.sourceforge.argparse4j.inf.Namespace; |
||||||
|
import org.apache.kafka.common.utils.Exit; |
||||||
|
import org.apache.kafka.common.utils.Utils; |
||||||
|
import org.apache.kafka.connect.runtime.WorkerConfig; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.ClassLoaderFactory; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.DelegatingClassLoader; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginDesc; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginScanResult; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginSource; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginType; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginUtils; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.ReflectionScanner; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.ServiceLoaderScanner; |
||||||
|
|
||||||
|
import java.io.BufferedReader; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.InputStreamReader; |
||||||
|
import java.io.PrintStream; |
||||||
|
import java.io.UncheckedIOException; |
||||||
|
import java.net.URI; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.net.URL; |
||||||
|
import java.net.URLConnection; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.nio.file.Paths; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.Enumeration; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.Properties; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
public class ConnectPluginPath { |
||||||
|
|
||||||
|
private static final String MANIFEST_PREFIX = "META-INF/services/"; |
||||||
|
public static final Object[] LIST_TABLE_COLUMNS = { |
||||||
|
"pluginName", |
||||||
|
"firstAlias", |
||||||
|
"secondAlias", |
||||||
|
"pluginVersion", |
||||||
|
"pluginType", |
||||||
|
"isLoadable", |
||||||
|
"hasManifest", |
||||||
|
"pluginLocation" // last because it is least important and most repetitive
|
||||||
|
}; |
||||||
|
public static final String NO_ALIAS = "N/A"; |
||||||
|
|
||||||
|
public static void main(String[] args) { |
||||||
|
Exit.exit(mainNoExit(args, System.out, System.err)); |
||||||
|
} |
||||||
|
|
||||||
|
public static int mainNoExit(String[] args, PrintStream out, PrintStream err) { |
||||||
|
ArgumentParser parser = parser(); |
||||||
|
try { |
||||||
|
Namespace namespace = parser.parseArgs(args); |
||||||
|
Config config = parseConfig(parser, namespace, out); |
||||||
|
runCommand(config); |
||||||
|
return 0; |
||||||
|
} catch (ArgumentParserException e) { |
||||||
|
parser.handleError(e); |
||||||
|
return 1; |
||||||
|
} catch (TerseException e) { |
||||||
|
err.println(e.getMessage()); |
||||||
|
return 2; |
||||||
|
} catch (Throwable e) { |
||||||
|
err.println(e.getMessage()); |
||||||
|
err.println(Utils.stackTrace(e)); |
||||||
|
return 3; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static ArgumentParser parser() { |
||||||
|
ArgumentParser parser = ArgumentParsers.newArgumentParser("connect-plugin-path") |
||||||
|
.defaultHelp(true) |
||||||
|
.description("Manage plugins on the Connect plugin.path"); |
||||||
|
|
||||||
|
ArgumentParser listCommand = parser.addSubparsers() |
||||||
|
.description("List information about plugins contained within the specified plugin locations") |
||||||
|
.dest("subcommand") |
||||||
|
.addParser("list"); |
||||||
|
|
||||||
|
ArgumentParser[] subparsers = new ArgumentParser[] { |
||||||
|
listCommand, |
||||||
|
}; |
||||||
|
|
||||||
|
for (ArgumentParser subparser : subparsers) { |
||||||
|
ArgumentGroup pluginProviders = subparser.addArgumentGroup("plugin providers"); |
||||||
|
pluginProviders.addArgument("--plugin-location") |
||||||
|
.setDefault(new ArrayList<>()) |
||||||
|
.action(Arguments.append()) |
||||||
|
.help("A single plugin location (jar file or directory)"); |
||||||
|
|
||||||
|
pluginProviders.addArgument("--plugin-path") |
||||||
|
.setDefault(new ArrayList<>()) |
||||||
|
.action(Arguments.append()) |
||||||
|
.help("A comma-delimited list of locations containing plugins"); |
||||||
|
|
||||||
|
pluginProviders.addArgument("--worker-config") |
||||||
|
.setDefault(new ArrayList<>()) |
||||||
|
.action(Arguments.append()) |
||||||
|
.help("A Connect worker configuration file"); |
||||||
|
} |
||||||
|
|
||||||
|
return parser; |
||||||
|
} |
||||||
|
|
||||||
|
private static Config parseConfig(ArgumentParser parser, Namespace namespace, PrintStream out) throws ArgumentParserException, TerseException { |
||||||
|
Set<Path> locations = parseLocations(parser, namespace); |
||||||
|
String subcommand = namespace.getString("subcommand"); |
||||||
|
if (subcommand == null) { |
||||||
|
throw new ArgumentParserException("No subcommand specified", parser); |
||||||
|
} |
||||||
|
switch (subcommand) { |
||||||
|
case "list": |
||||||
|
return new Config(Command.LIST, locations, out); |
||||||
|
default: |
||||||
|
throw new ArgumentParserException("Unrecognized subcommand: '" + subcommand + "'", parser); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Set<Path> parseLocations(ArgumentParser parser, Namespace namespace) throws ArgumentParserException, TerseException { |
||||||
|
List<String> rawLocations = new ArrayList<>(namespace.getList("plugin_location")); |
||||||
|
List<String> rawPluginPaths = new ArrayList<>(namespace.getList("plugin_path")); |
||||||
|
List<String> rawWorkerConfigs = new ArrayList<>(namespace.getList("worker_config")); |
||||||
|
if (rawLocations.isEmpty() && rawPluginPaths.isEmpty() && rawWorkerConfigs.isEmpty()) { |
||||||
|
throw new ArgumentParserException("Must specify at least one --plugin-location, --plugin-path, or --worker-config", parser); |
||||||
|
} |
||||||
|
Set<Path> pluginLocations = new LinkedHashSet<>(); |
||||||
|
for (String rawWorkerConfig : rawWorkerConfigs) { |
||||||
|
Properties properties; |
||||||
|
try { |
||||||
|
properties = Utils.loadProps(rawWorkerConfig); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new TerseException("Unable to read worker config at " + rawWorkerConfig); |
||||||
|
} |
||||||
|
String pluginPath = properties.getProperty(WorkerConfig.PLUGIN_PATH_CONFIG); |
||||||
|
if (pluginPath != null) { |
||||||
|
rawPluginPaths.add(pluginPath); |
||||||
|
} |
||||||
|
} |
||||||
|
for (String rawPluginPath : rawPluginPaths) { |
||||||
|
try { |
||||||
|
pluginLocations.addAll(PluginUtils.pluginLocations(rawPluginPath, true)); |
||||||
|
} catch (UncheckedIOException e) { |
||||||
|
throw new TerseException("Unable to parse plugin path " + rawPluginPath + ": " + e.getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
for (String rawLocation : rawLocations) { |
||||||
|
Path pluginLocation = Paths.get(rawLocation); |
||||||
|
if (!pluginLocation.toFile().exists()) { |
||||||
|
throw new TerseException("Specified location " + pluginLocation + " does not exist"); |
||||||
|
} |
||||||
|
pluginLocations.add(pluginLocation); |
||||||
|
} |
||||||
|
return pluginLocations; |
||||||
|
} |
||||||
|
|
||||||
|
enum Command { |
||||||
|
LIST |
||||||
|
} |
||||||
|
|
||||||
|
private static class Config { |
||||||
|
private final Command command; |
||||||
|
private final Set<Path> locations; |
||||||
|
private final PrintStream out; |
||||||
|
|
||||||
|
private Config(Command command, Set<Path> locations, PrintStream out) { |
||||||
|
this.command = command; |
||||||
|
this.locations = locations; |
||||||
|
this.out = out; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "Config{" + |
||||||
|
"command=" + command + |
||||||
|
", locations=" + locations + |
||||||
|
'}'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static void runCommand(Config config) throws TerseException { |
||||||
|
try { |
||||||
|
ClassLoader parent = ConnectPluginPath.class.getClassLoader(); |
||||||
|
ServiceLoaderScanner serviceLoaderScanner = new ServiceLoaderScanner(); |
||||||
|
ReflectionScanner reflectionScanner = new ReflectionScanner(); |
||||||
|
// Process the contents of the classpath to exclude it from later results.
|
||||||
|
PluginSource classpathSource = PluginUtils.classpathPluginSource(parent); |
||||||
|
Map<String, List<ManifestEntry>> classpathManifests = findManifests(classpathSource, Collections.emptyMap()); |
||||||
|
PluginScanResult classpathPlugins = discoverPlugins(classpathSource, reflectionScanner, serviceLoaderScanner); |
||||||
|
Map<Path, Set<Row>> rowsByLocation = new LinkedHashMap<>(); |
||||||
|
Set<Row> classpathRows = enumerateRows(null, classpathManifests, classpathPlugins); |
||||||
|
rowsByLocation.put(null, classpathRows); |
||||||
|
|
||||||
|
ClassLoaderFactory factory = new ClassLoaderFactory(); |
||||||
|
try (DelegatingClassLoader delegatingClassLoader = factory.newDelegatingClassLoader(parent)) { |
||||||
|
beginCommand(config); |
||||||
|
for (Path pluginLocation : config.locations) { |
||||||
|
PluginSource source = PluginUtils.isolatedPluginSource(pluginLocation, delegatingClassLoader, factory); |
||||||
|
Map<String, List<ManifestEntry>> manifests = findManifests(source, classpathManifests); |
||||||
|
PluginScanResult plugins = discoverPlugins(source, reflectionScanner, serviceLoaderScanner); |
||||||
|
Set<Row> rows = enumerateRows(pluginLocation, manifests, plugins); |
||||||
|
rowsByLocation.put(pluginLocation, rows); |
||||||
|
for (Row row : rows) { |
||||||
|
handlePlugin(config, row); |
||||||
|
} |
||||||
|
} |
||||||
|
endCommand(config, rowsByLocation); |
||||||
|
} |
||||||
|
} catch (IOException e) { |
||||||
|
throw new UncheckedIOException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The unit of work for a command. |
||||||
|
* <p>This is unique to the (source, class, type) tuple, and contains additional pre-computed information |
||||||
|
* that pertains to this specific plugin. |
||||||
|
*/ |
||||||
|
private static class Row { |
||||||
|
private final Path pluginLocation; |
||||||
|
private final String className; |
||||||
|
private final PluginType type; |
||||||
|
private final String version; |
||||||
|
private final List<String> aliases; |
||||||
|
private final boolean loadable; |
||||||
|
private final boolean hasManifest; |
||||||
|
|
||||||
|
public Row(Path pluginLocation, String className, PluginType type, String version, List<String> aliases, boolean loadable, boolean hasManifest) { |
||||||
|
this.pluginLocation = pluginLocation; |
||||||
|
this.className = Objects.requireNonNull(className, "className must be non-null"); |
||||||
|
this.version = Objects.requireNonNull(version, "version must be non-null"); |
||||||
|
this.type = Objects.requireNonNull(type, "type must be non-null"); |
||||||
|
this.aliases = Objects.requireNonNull(aliases, "aliases must be non-null"); |
||||||
|
this.loadable = loadable; |
||||||
|
this.hasManifest = hasManifest; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean loadable() { |
||||||
|
return loadable; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean compatible() { |
||||||
|
return loadable && hasManifest; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean incompatible() { |
||||||
|
return !compatible(); |
||||||
|
} |
||||||
|
|
||||||
|
private String locationString() { |
||||||
|
return pluginLocation == null ? "classpath" : pluginLocation.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(Object o) { |
||||||
|
if (this == o) return true; |
||||||
|
if (o == null || getClass() != o.getClass()) return false; |
||||||
|
Row row = (Row) o; |
||||||
|
return Objects.equals(pluginLocation, row.pluginLocation) && className.equals(row.className) && type == row.type; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return Objects.hash(pluginLocation, className, type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Set<Row> enumerateRows(Path pluginLocation, Map<String, List<ManifestEntry>> manifests, PluginScanResult scanResult) { |
||||||
|
Set<Row> rows = new HashSet<>(); |
||||||
|
// Perform a deep copy of the manifests because we're going to be mutating our copy.
|
||||||
|
Map<String, Set<ManifestEntry>> unloadablePlugins = manifests.entrySet().stream() |
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, e -> new HashSet<>(e.getValue()))); |
||||||
|
scanResult.forEach(pluginDesc -> { |
||||||
|
// Emit a loadable row for this scan result, since it was found during plugin discovery
|
||||||
|
Set<String> rowAliases = new LinkedHashSet<>(); |
||||||
|
rowAliases.add(PluginUtils.simpleName(pluginDesc)); |
||||||
|
rowAliases.add(PluginUtils.prunedName(pluginDesc)); |
||||||
|
rows.add(newRow(pluginLocation, pluginDesc.className(), new ArrayList<>(rowAliases), pluginDesc.type(), pluginDesc.version(), true, manifests)); |
||||||
|
// Remove the ManifestEntry if it has the same className and type as one of the loadable plugins.
|
||||||
|
unloadablePlugins.getOrDefault(pluginDesc.className(), Collections.emptySet()).removeIf(entry -> entry.type == pluginDesc.type()); |
||||||
|
}); |
||||||
|
unloadablePlugins.values().forEach(entries -> entries.forEach(entry -> { |
||||||
|
// Emit a non-loadable row, since all the loadable rows showed up in the previous iteration.
|
||||||
|
// Two ManifestEntries may produce the same row if they have different URIs
|
||||||
|
rows.add(newRow(pluginLocation, entry.className, Collections.emptyList(), entry.type, PluginDesc.UNDEFINED_VERSION, false, manifests)); |
||||||
|
})); |
||||||
|
return rows; |
||||||
|
} |
||||||
|
|
||||||
|
private static Row newRow(Path pluginLocation, String className, List<String> rowAliases, PluginType type, String version, boolean loadable, Map<String, List<ManifestEntry>> manifests) { |
||||||
|
boolean hasManifest = manifests.containsKey(className) && manifests.get(className).stream().anyMatch(e -> e.type == type); |
||||||
|
return new Row(pluginLocation, className, type, version, rowAliases, loadable, hasManifest); |
||||||
|
} |
||||||
|
|
||||||
|
private static void beginCommand(Config config) { |
||||||
|
if (config.command == Command.LIST) { |
||||||
|
// The list command prints a TSV-formatted table with details of the found plugins
|
||||||
|
// This is officially human-readable output with no guarantees for backwards-compatibility
|
||||||
|
// It should be reasonably easy to parse for ad-hoc scripting use-cases.
|
||||||
|
listTablePrint(config, LIST_TABLE_COLUMNS); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void handlePlugin(Config config, Row row) { |
||||||
|
if (config.command == Command.LIST) { |
||||||
|
String firstAlias = row.aliases.size() > 0 ? row.aliases.get(0) : NO_ALIAS; |
||||||
|
String secondAlias = row.aliases.size() > 1 ? row.aliases.get(1) : NO_ALIAS; |
||||||
|
listTablePrint(config, |
||||||
|
row.className, |
||||||
|
firstAlias, |
||||||
|
secondAlias, |
||||||
|
row.version, |
||||||
|
row.type, |
||||||
|
row.loadable, |
||||||
|
row.hasManifest, |
||||||
|
// last because it is least important and most repetitive
|
||||||
|
row.locationString() |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void endCommand( |
||||||
|
Config config, |
||||||
|
Map<Path, Set<Row>> rowsByLocation |
||||||
|
) { |
||||||
|
if (config.command == Command.LIST) { |
||||||
|
// end the table with an empty line to enable users to separate the table from the summary.
|
||||||
|
config.out.println(); |
||||||
|
rowsByLocation.remove(null); |
||||||
|
Set<Row> isolatedRows = rowsByLocation.values().stream().flatMap(Set::stream).collect(Collectors.toSet()); |
||||||
|
long totalPlugins = isolatedRows.size(); |
||||||
|
long loadablePlugins = isolatedRows.stream().filter(Row::loadable).count(); |
||||||
|
long compatiblePlugins = isolatedRows.stream().filter(Row::compatible).count(); |
||||||
|
config.out.printf("Total plugins: \t%d%n", totalPlugins); |
||||||
|
config.out.printf("Loadable plugins: \t%d%n", loadablePlugins); |
||||||
|
config.out.printf("Compatible plugins: \t%d%n", compatiblePlugins); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void listTablePrint(Config config, Object... args) { |
||||||
|
if (ConnectPluginPath.LIST_TABLE_COLUMNS.length != args.length) { |
||||||
|
throw new IllegalArgumentException("Table must have exactly " + ConnectPluginPath.LIST_TABLE_COLUMNS.length + " columns"); |
||||||
|
} |
||||||
|
config.out.println(Stream.of(args) |
||||||
|
.map(Objects::toString) |
||||||
|
.collect(Collectors.joining("\t"))); |
||||||
|
} |
||||||
|
|
||||||
|
private static PluginScanResult discoverPlugins(PluginSource source, ReflectionScanner reflectionScanner, ServiceLoaderScanner serviceLoaderScanner) { |
||||||
|
PluginScanResult serviceLoadResult = serviceLoaderScanner.discoverPlugins(Collections.singleton(source)); |
||||||
|
PluginScanResult reflectiveResult = reflectionScanner.discoverPlugins(Collections.singleton(source)); |
||||||
|
return new PluginScanResult(Arrays.asList(serviceLoadResult, reflectiveResult)); |
||||||
|
} |
||||||
|
|
||||||
|
private static class ManifestEntry { |
||||||
|
private final URI manifestURI; |
||||||
|
private final String className; |
||||||
|
private final PluginType type; |
||||||
|
|
||||||
|
private ManifestEntry(URI manifestURI, String className, PluginType type) { |
||||||
|
this.manifestURI = manifestURI; |
||||||
|
this.className = className; |
||||||
|
this.type = type; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(Object o) { |
||||||
|
if (this == o) return true; |
||||||
|
if (o == null || getClass() != o.getClass()) return false; |
||||||
|
ManifestEntry that = (ManifestEntry) o; |
||||||
|
return manifestURI.equals(that.manifestURI) && className.equals(that.className) && type == that.type; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return Objects.hash(manifestURI, className, type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Map<String, List<ManifestEntry>> findManifests(PluginSource source, Map<String, List<ManifestEntry>> exclude) { |
||||||
|
Map<String, List<ManifestEntry>> manifests = new LinkedHashMap<>(); |
||||||
|
for (PluginType type : PluginType.values()) { |
||||||
|
try { |
||||||
|
Enumeration<URL> resources = source.loader().getResources(MANIFEST_PREFIX + type.superClass().getName()); |
||||||
|
while (resources.hasMoreElements()) { |
||||||
|
URL url = resources.nextElement(); |
||||||
|
for (String className : parse(url)) { |
||||||
|
ManifestEntry e = new ManifestEntry(url.toURI(), className, type); |
||||||
|
manifests.computeIfAbsent(className, ignored -> new ArrayList<>()).add(e); |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (URISyntaxException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new UncheckedIOException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
for (Map.Entry<String, List<ManifestEntry>> entry : exclude.entrySet()) { |
||||||
|
String className = entry.getKey(); |
||||||
|
List<ManifestEntry> excluded = entry.getValue(); |
||||||
|
// Note this must be a remove and not removeAll, because we want to remove only one copy at a time.
|
||||||
|
// If the same jar is present on the classpath and plugin path, then manifests will contain 2 identical
|
||||||
|
// ManifestEntry instances, with a third copy in the excludes. After the excludes are processed,
|
||||||
|
// manifests should contain exactly one copy of the ManifestEntry.
|
||||||
|
for (ManifestEntry e : excluded) { |
||||||
|
manifests.getOrDefault(className, Collections.emptyList()).remove(e); |
||||||
|
} |
||||||
|
} |
||||||
|
return manifests; |
||||||
|
} |
||||||
|
|
||||||
|
// Based on implementation from ServiceLoader.LazyClassPathLookupIterator from OpenJDK11
|
||||||
|
private static Set<String> parse(URL u) { |
||||||
|
Set<String> names = new LinkedHashSet<>(); // preserve insertion order
|
||||||
|
try { |
||||||
|
URLConnection uc = u.openConnection(); |
||||||
|
uc.setUseCaches(false); |
||||||
|
try (InputStream in = uc.getInputStream(); |
||||||
|
BufferedReader r |
||||||
|
= new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { |
||||||
|
int lc = 1; |
||||||
|
while ((lc = parseLine(u, r, lc, names)) >= 0) { |
||||||
|
// pass
|
||||||
|
} |
||||||
|
} |
||||||
|
} catch (IOException x) { |
||||||
|
throw new RuntimeException("Error accessing configuration file", x); |
||||||
|
} |
||||||
|
return names; |
||||||
|
} |
||||||
|
|
||||||
|
// Based on implementation from ServiceLoader.LazyClassPathLookupIterator from OpenJDK11
|
||||||
|
private static int parseLine(URL u, BufferedReader r, int lc, Set<String> names) throws IOException { |
||||||
|
String ln = r.readLine(); |
||||||
|
if (ln == null) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
int ci = ln.indexOf('#'); |
||||||
|
if (ci >= 0) ln = ln.substring(0, ci); |
||||||
|
ln = ln.trim(); |
||||||
|
int n = ln.length(); |
||||||
|
if (n != 0) { |
||||||
|
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) |
||||||
|
throw new IOException("Illegal configuration-file syntax in " + u); |
||||||
|
int cp = ln.codePointAt(0); |
||||||
|
if (!Character.isJavaIdentifierStart(cp)) |
||||||
|
throw new IOException("Illegal provider-class name: " + ln + " in " + u); |
||||||
|
int start = Character.charCount(cp); |
||||||
|
for (int i = start; i < n; i += Character.charCount(cp)) { |
||||||
|
cp = ln.codePointAt(i); |
||||||
|
if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) |
||||||
|
throw new IOException("Illegal provider-class name: " + ln + " in " + u); |
||||||
|
} |
||||||
|
names.add(ln); |
||||||
|
} |
||||||
|
return lc + 1; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,516 @@ |
|||||||
|
/* |
||||||
|
* 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.kafka.tools; |
||||||
|
|
||||||
|
import org.apache.kafka.connect.runtime.isolation.ClassLoaderFactory; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.DelegatingClassLoader; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginScanResult; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginSource; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.PluginUtils; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.ReflectionScanner; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.ServiceLoaderScanner; |
||||||
|
import org.apache.kafka.connect.runtime.isolation.TestPlugins; |
||||||
|
import org.junit.jupiter.api.BeforeAll; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.io.TempDir; |
||||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||||
|
import org.junit.jupiter.params.provider.EnumSource; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.OutputStream; |
||||||
|
import java.io.PrintStream; |
||||||
|
import java.io.UncheckedIOException; |
||||||
|
import java.io.UnsupportedEncodingException; |
||||||
|
import java.net.MalformedURLException; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
import java.nio.file.Files; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.nio.file.Paths; |
||||||
|
import java.nio.file.StandardCopyOption; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.Properties; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.jar.JarFile; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals; |
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals; |
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue; |
||||||
|
|
||||||
|
public class ConnectPluginPathTest { |
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ConnectPluginPathTest.class); |
||||||
|
|
||||||
|
private static final int NAME_COL = 0; |
||||||
|
private static final int ALIAS1_COL = 1; |
||||||
|
private static final int ALIAS2_COL = 2; |
||||||
|
private static final int VERSION_COL = 3; |
||||||
|
private static final int TYPE_COL = 4; |
||||||
|
private static final int LOADABLE_COL = 5; |
||||||
|
private static final int MANIFEST_COL = 6; |
||||||
|
private static final int LOCATION_COL = 7; |
||||||
|
|
||||||
|
@TempDir |
||||||
|
public Path workspace; |
||||||
|
|
||||||
|
@BeforeAll |
||||||
|
public static void setUp() { |
||||||
|
// Work around a circular-dependency in TestPlugins.
|
||||||
|
TestPlugins.pluginPath(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void testNoArguments() { |
||||||
|
CommandResult res = runCommand(); |
||||||
|
assertNotEquals(0, res.returnCode); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testListNoArguments() { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list" |
||||||
|
); |
||||||
|
assertNotEquals(0, res.returnCode); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@EnumSource |
||||||
|
public void testListOneLocation(PluginLocationType type) { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list", |
||||||
|
"--plugin-location", |
||||||
|
setupLocation(workspace.resolve("location-a"), type, TestPlugins.TestPlugin.NON_MIGRATED_MULTI_PLUGIN) |
||||||
|
); |
||||||
|
Map<String, List<String[]>> table = assertListSuccess(res); |
||||||
|
assertNonMigratedPluginsPresent(table); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@EnumSource |
||||||
|
public void testListMultipleLocations(PluginLocationType type) { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list", |
||||||
|
"--plugin-location", |
||||||
|
setupLocation(workspace.resolve("location-a"), type, TestPlugins.TestPlugin.NON_MIGRATED_MULTI_PLUGIN), |
||||||
|
"--plugin-location", |
||||||
|
setupLocation(workspace.resolve("location-b"), type, TestPlugins.TestPlugin.SAMPLING_CONFIGURABLE) |
||||||
|
); |
||||||
|
Map<String, List<String[]>> table = assertListSuccess(res); |
||||||
|
assertNonMigratedPluginsPresent(table); |
||||||
|
assertPluginsAreCompatible(table, |
||||||
|
TestPlugins.TestPlugin.SAMPLING_CONFIGURABLE); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@EnumSource |
||||||
|
public void testListOnePluginPath(PluginLocationType type) { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list", |
||||||
|
"--plugin-path", |
||||||
|
setupPluginPathElement(workspace.resolve("path-a"), type, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_MULTI_PLUGIN, TestPlugins.TestPlugin.SAMPLING_CONFIGURABLE) |
||||||
|
); |
||||||
|
Map<String, List<String[]>> table = assertListSuccess(res); |
||||||
|
assertPluginsAreCompatible(table, |
||||||
|
TestPlugins.TestPlugin.SAMPLING_CONFIGURABLE); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@EnumSource |
||||||
|
public void testListMultiplePluginPaths(PluginLocationType type) { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list", |
||||||
|
"--plugin-path", |
||||||
|
setupPluginPathElement(workspace.resolve("path-a"), type, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_MULTI_PLUGIN, TestPlugins.TestPlugin.SAMPLING_CONFIGURABLE), |
||||||
|
"--plugin-path", |
||||||
|
setupPluginPathElement(workspace.resolve("path-b"), type, |
||||||
|
TestPlugins.TestPlugin.SAMPLING_HEADER_CONVERTER, TestPlugins.TestPlugin.ALIASED_STATIC_FIELD) |
||||||
|
); |
||||||
|
Map<String, List<String[]>> table = assertListSuccess(res); |
||||||
|
assertPluginsAreCompatible(table, |
||||||
|
TestPlugins.TestPlugin.SAMPLING_CONFIGURABLE, |
||||||
|
TestPlugins.TestPlugin.ALIASED_STATIC_FIELD); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@EnumSource |
||||||
|
public void testListOneWorkerConfig(PluginLocationType type) { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list", |
||||||
|
"--worker-config", |
||||||
|
setupWorkerConfig(workspace.resolve("worker.properties"), |
||||||
|
setupPluginPathElement(workspace.resolve("path-a"), type, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_CO_LOCATED)) |
||||||
|
); |
||||||
|
Map<String, List<String[]>> table = assertListSuccess(res); |
||||||
|
assertBadPackagingPluginsPresent(table); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@EnumSource |
||||||
|
public void testListMultipleWorkerConfigs(PluginLocationType type) { |
||||||
|
CommandResult res = runCommand( |
||||||
|
"list", |
||||||
|
"--worker-config", |
||||||
|
setupWorkerConfig(workspace.resolve("worker-a.properties"), |
||||||
|
setupPluginPathElement(workspace.resolve("path-a"), type, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_MULTI_PLUGIN)), |
||||||
|
"--worker-config", |
||||||
|
setupWorkerConfig(workspace.resolve("worker-b.properties"), |
||||||
|
setupPluginPathElement(workspace.resolve("path-b"), type, |
||||||
|
TestPlugins.TestPlugin.SERVICE_LOADER)) |
||||||
|
); |
||||||
|
Map<String, List<String[]>> table = assertListSuccess(res); |
||||||
|
assertNonMigratedPluginsPresent(table); |
||||||
|
assertPluginsAreCompatible(table, |
||||||
|
TestPlugins.TestPlugin.SERVICE_LOADER); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static Map<String, List<String[]>> assertListSuccess(CommandResult result) { |
||||||
|
assertEquals(0, result.returnCode); |
||||||
|
Map<String, List<String[]>> table = parseTable(result.out); |
||||||
|
assertIsolatedPluginsInOutput(result.reflective, table); |
||||||
|
return table; |
||||||
|
} |
||||||
|
|
||||||
|
private static void assertPluginsAreCompatible(Map<String, List<String[]>> table, TestPlugins.TestPlugin... plugins) { |
||||||
|
assertPluginMigrationStatus(table, true, true, plugins); |
||||||
|
} |
||||||
|
|
||||||
|
private static void assertNonMigratedPluginsPresent(Map<String, List<String[]>> table) { |
||||||
|
assertPluginMigrationStatus(table, true, false, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_CONVERTER, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_HEADER_CONVERTER, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_PREDICATE, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_SINK_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_SOURCE_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_TRANSFORMATION); |
||||||
|
// This plugin is partially compatible
|
||||||
|
assertPluginMigrationStatus(table, true, null, |
||||||
|
TestPlugins.TestPlugin.NON_MIGRATED_MULTI_PLUGIN); |
||||||
|
} |
||||||
|
|
||||||
|
private static void assertBadPackagingPluginsPresent(Map<String, List<String[]>> table) { |
||||||
|
assertPluginsAreCompatible(table, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_CO_LOCATED, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_VERSION_METHOD_THROWS_CONNECTOR); |
||||||
|
assertPluginMigrationStatus(table, false, true, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_MISSING_SUPERCLASS, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_STATIC_INITIALIZER_THROWS_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_DEFAULT_CONSTRUCTOR_THROWS_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_DEFAULT_CONSTRUCTOR_PRIVATE_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_NO_DEFAULT_CONSTRUCTOR_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_NO_DEFAULT_CONSTRUCTOR_CONVERTER, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_NO_DEFAULT_CONSTRUCTOR_OVERRIDE_POLICY, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_INNER_CLASS_CONNECTOR, |
||||||
|
TestPlugins.TestPlugin.BAD_PACKAGING_STATIC_INITIALIZER_THROWS_REST_EXTENSION); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static void assertIsolatedPluginsInOutput(PluginScanResult reflectiveResult, Map<String, List<String[]>> table) { |
||||||
|
reflectiveResult.forEach(pluginDesc -> { |
||||||
|
if (pluginDesc.location().equals("classpath")) { |
||||||
|
// Classpath plugins do not appear in list output
|
||||||
|
return; |
||||||
|
} |
||||||
|
assertTrue(table.containsKey(pluginDesc.className()), "Plugin " + pluginDesc.className() + " does not appear in list output"); |
||||||
|
boolean foundType = false; |
||||||
|
for (String[] row : table.get(pluginDesc.className())) { |
||||||
|
if (row[TYPE_COL].equals(pluginDesc.typeName())) { |
||||||
|
foundType = true; |
||||||
|
assertTrue(row[ALIAS1_COL].equals(ConnectPluginPath.NO_ALIAS) || row[ALIAS1_COL].equals(PluginUtils.simpleName(pluginDesc))); |
||||||
|
assertTrue(row[ALIAS2_COL].equals(ConnectPluginPath.NO_ALIAS) || row[ALIAS2_COL].equals(PluginUtils.prunedName(pluginDesc))); |
||||||
|
assertEquals(pluginDesc.version(), row[VERSION_COL]); |
||||||
|
try { |
||||||
|
Path pluginLocation = Paths.get(row[LOCATION_COL]); |
||||||
|
// This transforms the raw path `/path/to/somewhere` to the url `file:/path/to/somewhere`
|
||||||
|
String pluginLocationUrl = pluginLocation.toUri().toURL().toString(); |
||||||
|
assertEquals(pluginDesc.location(), pluginLocationUrl); |
||||||
|
} catch (MalformedURLException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
assertTrue(foundType, "Plugin " + pluginDesc.className() + " does not have row for " + pluginDesc.typeName()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private static void assertPluginMigrationStatus(Map<String, List<String[]>> table, Boolean loadable, Boolean compatible, TestPlugins.TestPlugin... plugins) { |
||||||
|
for (TestPlugins.TestPlugin plugin : plugins) { |
||||||
|
assertTrue(table.containsKey(plugin.className()), "Plugin " + plugin.className() + " does not appear in list output"); |
||||||
|
for (String[] row : table.get(plugin.className())) { |
||||||
|
log.info("row" + Arrays.toString(row)); |
||||||
|
if (loadable != null) { |
||||||
|
assertEquals(loadable, Boolean.parseBoolean(row[LOADABLE_COL]), "Plugin loadable column for " + plugin.className() + " incorrect"); |
||||||
|
} |
||||||
|
if (compatible != null) { |
||||||
|
assertEquals(compatible, Boolean.parseBoolean(row[MANIFEST_COL]), "Plugin hasManifest column for " + plugin.className() + " incorrect"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private enum PluginLocationType { |
||||||
|
CLASS_HIERARCHY, |
||||||
|
SINGLE_JAR, |
||||||
|
MULTI_JAR |
||||||
|
} |
||||||
|
|
||||||
|
private static class PluginLocation { |
||||||
|
private final Path path; |
||||||
|
|
||||||
|
private PluginLocation(Path path) { |
||||||
|
this.path = path; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return path.toString(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Populate a writable disk path to be usable as a single plugin location. |
||||||
|
* The returned path will be usable as a single path. |
||||||
|
* @param path A non-existent path immediately within a writable directory, suggesting a location for this plugin. |
||||||
|
* @param type The format to which the on-disk plugin should conform |
||||||
|
* @param plugin The plugin which should be written to the specified path |
||||||
|
* @return The final usable path name to this location, in case it is different from the suggested input path. |
||||||
|
*/ |
||||||
|
private static PluginLocation setupLocation(Path path, PluginLocationType type, TestPlugins.TestPlugin plugin) { |
||||||
|
try { |
||||||
|
Path jarPath = TestPlugins.pluginPath(plugin).stream().findFirst().get(); |
||||||
|
switch (type) { |
||||||
|
case CLASS_HIERARCHY: { |
||||||
|
try (JarFile jarFile = new JarFile(jarPath.toFile())) { |
||||||
|
jarFile.stream().forEach(jarEntry -> { |
||||||
|
Path entryPath = path.resolve(jarEntry.getName()); |
||||||
|
try { |
||||||
|
entryPath.getParent().toFile().mkdirs(); |
||||||
|
Files.copy(jarFile.getInputStream(jarEntry), entryPath, StandardCopyOption.REPLACE_EXISTING); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new UncheckedIOException(e); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
return new PluginLocation(path); |
||||||
|
} |
||||||
|
case SINGLE_JAR: { |
||||||
|
Path outputJar = path.resolveSibling(path.getFileName() + ".jar"); |
||||||
|
outputJar.getParent().toFile().mkdirs(); |
||||||
|
Files.copy(jarPath, outputJar, StandardCopyOption.REPLACE_EXISTING); |
||||||
|
return new PluginLocation(outputJar); |
||||||
|
} |
||||||
|
case MULTI_JAR: { |
||||||
|
Path outputJar = path.resolve(jarPath.getFileName()); |
||||||
|
outputJar.getParent().toFile().mkdirs(); |
||||||
|
Files.copy(jarPath, outputJar, StandardCopyOption.REPLACE_EXISTING); |
||||||
|
return new PluginLocation(path); |
||||||
|
} |
||||||
|
default: |
||||||
|
throw new IllegalArgumentException("Unknown PluginLocationType"); |
||||||
|
} |
||||||
|
} catch (IOException e) { |
||||||
|
throw new UncheckedIOException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static class PluginPathElement { |
||||||
|
private final Path root; |
||||||
|
private final List<PluginLocation> locations; |
||||||
|
|
||||||
|
private PluginPathElement(Path root, List<PluginLocation> locations) { |
||||||
|
this.root = root; |
||||||
|
this.locations = locations; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return root.toString(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Populate a writable disk path to be usable as single {@code plugin.path} element providing the specified plugins |
||||||
|
* @param path A directory that should contain the populated plugins, will be created if it does not exist. |
||||||
|
* @param type The format to which the on-disk plugins should conform |
||||||
|
* @param plugins The plugins which should be written to the specified path |
||||||
|
* @return The specific inner locations of the plugins that were written. |
||||||
|
*/ |
||||||
|
private PluginPathElement setupPluginPathElement(Path path, PluginLocationType type, TestPlugins.TestPlugin... plugins) { |
||||||
|
List<PluginLocation> locations = new ArrayList<>(); |
||||||
|
for (int i = 0; i < plugins.length; i++) { |
||||||
|
TestPlugins.TestPlugin plugin = plugins[i]; |
||||||
|
locations.add(setupLocation(path.resolve("plugin-" + i), type, plugin)); |
||||||
|
} |
||||||
|
return new PluginPathElement(path, locations); |
||||||
|
} |
||||||
|
|
||||||
|
private static class WorkerConfig { |
||||||
|
private final Path configFile; |
||||||
|
private final List<PluginPathElement> pluginPathElements; |
||||||
|
|
||||||
|
private WorkerConfig(Path configFile, List<PluginPathElement> pluginPathElements) { |
||||||
|
this.configFile = configFile; |
||||||
|
this.pluginPathElements = pluginPathElements; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return configFile.toString(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Populate a writable disk path |
||||||
|
* @param path |
||||||
|
* @param pluginPathElements |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
private static WorkerConfig setupWorkerConfig(Path path, PluginPathElement... pluginPathElements) { |
||||||
|
path.getParent().toFile().mkdirs(); |
||||||
|
Properties properties = new Properties(); |
||||||
|
String pluginPath = Arrays.stream(pluginPathElements) |
||||||
|
.map(Object::toString) |
||||||
|
.collect(Collectors.joining(", ")); |
||||||
|
properties.setProperty("plugin.path", pluginPath); |
||||||
|
try (OutputStream outputStream = Files.newOutputStream(path)) { |
||||||
|
properties.store(outputStream, "dummy worker properties file"); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new UncheckedIOException(e); |
||||||
|
} |
||||||
|
return new WorkerConfig(path, Arrays.asList(pluginPathElements)); |
||||||
|
} |
||||||
|
|
||||||
|
private static class CommandResult { |
||||||
|
public CommandResult(int returnCode, String out, String err, PluginScanResult reflective, PluginScanResult serviceLoading) { |
||||||
|
this.returnCode = returnCode; |
||||||
|
this.out = out; |
||||||
|
this.err = err; |
||||||
|
this.reflective = reflective; |
||||||
|
this.serviceLoading = serviceLoading; |
||||||
|
} |
||||||
|
|
||||||
|
int returnCode; |
||||||
|
String out; |
||||||
|
String err; |
||||||
|
PluginScanResult reflective; |
||||||
|
PluginScanResult serviceLoading; |
||||||
|
} |
||||||
|
|
||||||
|
private static CommandResult runCommand(Object... args) { |
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||||
|
ByteArrayOutputStream err = new ByteArrayOutputStream(); |
||||||
|
try { |
||||||
|
int returnCode = ConnectPluginPath.mainNoExit( |
||||||
|
Arrays.stream(args) |
||||||
|
.map(Object::toString) |
||||||
|
.collect(Collectors.toList()) |
||||||
|
.toArray(new String[]{}), |
||||||
|
new PrintStream(out, true, "utf-8"), |
||||||
|
new PrintStream(err, true, "utf-8")); |
||||||
|
Set<Path> pluginLocations = getPluginLocations(args); |
||||||
|
ClassLoader parent = ConnectPluginPath.class.getClassLoader(); |
||||||
|
ClassLoaderFactory factory = new ClassLoaderFactory(); |
||||||
|
try (DelegatingClassLoader delegatingClassLoader = factory.newDelegatingClassLoader(parent)) { |
||||||
|
Set<PluginSource> sources = PluginUtils.pluginSources(pluginLocations, delegatingClassLoader, factory); |
||||||
|
String stdout = new String(out.toByteArray(), StandardCharsets.UTF_8); |
||||||
|
String stderr = new String(err.toByteArray(), StandardCharsets.UTF_8); |
||||||
|
log.info("STDOUT:\n{}", stdout); |
||||||
|
log.info("STDERR:\n{}", stderr); |
||||||
|
return new CommandResult( |
||||||
|
returnCode, |
||||||
|
stdout, |
||||||
|
stderr, |
||||||
|
new ReflectionScanner().discoverPlugins(sources), |
||||||
|
new ServiceLoaderScanner().discoverPlugins(sources) |
||||||
|
); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} catch (UnsupportedEncodingException e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static Set<Path> getPluginLocations(Object[] args) { |
||||||
|
return Arrays.stream(args) |
||||||
|
.flatMap(obj -> { |
||||||
|
if (obj instanceof WorkerConfig) { |
||||||
|
return ((WorkerConfig) obj).pluginPathElements.stream(); |
||||||
|
} else { |
||||||
|
return Stream.of(obj); |
||||||
|
} |
||||||
|
}) |
||||||
|
.flatMap(obj -> { |
||||||
|
if (obj instanceof PluginPathElement) { |
||||||
|
return ((PluginPathElement) obj).locations.stream(); |
||||||
|
} else { |
||||||
|
return Stream.of(obj); |
||||||
|
} |
||||||
|
}) |
||||||
|
.map(obj -> { |
||||||
|
if (obj instanceof PluginLocation) { |
||||||
|
return ((PluginLocation) obj).path; |
||||||
|
} else { |
||||||
|
return null; |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
.filter(Objects::nonNull) |
||||||
|
.collect(Collectors.toSet()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Parse the main table of the list command. |
||||||
|
* <p>Map is keyed on the plugin name, with a list of rows which referred to that name if there are multiple. |
||||||
|
* Each row is pre-split into columns. |
||||||
|
* @param listOutput An executed list command |
||||||
|
* @return A parsed form of the table grouped by plugin class names |
||||||
|
*/ |
||||||
|
private static Map<String, List<String[]>> parseTable(String listOutput) { |
||||||
|
// Split on the empty line which should appear in the output.
|
||||||
|
String[] sections = listOutput.split("\n\\s*\n"); |
||||||
|
assertTrue(sections.length > 1, "No empty line in list output"); |
||||||
|
String[] rows = sections[0].split("\n"); |
||||||
|
Map<String, List<String[]>> table = new HashMap<>(); |
||||||
|
// Assert that the first row is the header
|
||||||
|
assertArrayEquals(ConnectPluginPath.LIST_TABLE_COLUMNS, rows[0].split("\t"), "Table header doesn't have the right columns"); |
||||||
|
// Skip the header to parse the rows in the table.
|
||||||
|
for (int i = 1; i < rows.length; i++) { |
||||||
|
// group rows by
|
||||||
|
String[] row = rows[i].split("\t"); |
||||||
|
assertEquals(ConnectPluginPath.LIST_TABLE_COLUMNS.length, row.length, "Table row is the wrong length"); |
||||||
|
table.computeIfAbsent(row[NAME_COL], ignored -> new ArrayList<>()).add(row); |
||||||
|
} |
||||||
|
return table; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue