diff --git a/pom.xml b/pom.xml index 3629efc24..a87ea1d3a 100644 --- a/pom.xml +++ b/pom.xml @@ -1290,147 +1290,66 @@ - com.google.cloud - google-cloud-storage - ${google.cloud.storage.version} - + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} + + + + org.apache.httpcomponents + httpclient + ${httpcomponents.version} + + + + com.google.api-client + google-api-client + 2.7.2 + + + com.google.auth + google-auth-library-oauth2-http + 1.50.0 - io.grpc - grpc-inprocess - - - io.grpc - grpc-alts - - - io.grpc - grpc-auth - - - io.grpc - grpc-core - - - io.grpc - grpc-protobuf - - - io.grpc - grpc-protobuf-lite - - - io.grpc - grpc-opentelemetry - - - io.grpc - grpc-grpclb - - - io.grpc - grpc-netty-shaded - - - io.grpc - grpc-util - - - io.grpc - grpc-stub - - - io.grpc - grpc-googleapis - - - io.grpc - grpc-xds - - - io.grpc - grpc-services - - - io.grpc - grpc-rls - - - com.google.api - gax-grpc - - - com.google.cloud - google-cloud-core-grpc - - - com.google.cloud - google-cloud-monitoring - - - com.google.api.grpc - proto-google-cloud-monitoring-v3 - - - com.google.api.grpc - grpc-google-cloud-storage-v2 - - - com.google.api.grpc - gapic-google-cloud-storage-v2 - - - com.google.cloud.opentelemetry - exporter-metrics - - - io.opentelemetry.contrib - opentelemetry-gcp-resources - - - io.opentelemetry - opentelemetry-sdk - - - io.opentelemetry - opentelemetry-sdk-trace - - - io.opentelemetry - opentelemetry-sdk-logs - - - io.opentelemetry - opentelemetry-sdk-metrics - - - io.opentelemetry - opentelemetry-sdk-common - - - io.opentelemetry - opentelemetry-sdk-extension-autoconfigure-spi + + javax.annotation + javax.annotation-api - org.apache.httpcomponents.client5 - httpclient5 - ${httpclient5.version} + + com.google.auth + google-auth-library-credentials + 1.50.0 + + + + com.google.http-client + google-http-client-apache-v2 + ${google.http.client.version} com.squareup.okhttp3 @@ -1547,6 +1466,18 @@ org.bouncycastle bcprov-jdk15on + + + com.google.cloud + google-cloud-storage + diff --git a/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java b/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java index 88536a882..549b46ae8 100644 --- a/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java +++ b/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java @@ -443,7 +443,7 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction /** The key of the configuration. e.g. http,https */ String CRAWLER_WEB_PROTOCOLS = "crawler.web.protocols"; - /** The key of the configuration. e.g. file,smb,smb1,ftp,s3,gcs */ + /** The key of the configuration. e.g. file,smb,smb1,ftp,s3 */ String CRAWLER_FILE_PROTOCOLS = "crawler.file.protocols"; /** The key of the configuration. e.g. ^FESS_ENV_.* */ @@ -3571,7 +3571,7 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction /** * Get the value for the key 'crawler.file.protocols'.
- * The value is, e.g. file,smb,smb1,ftp,s3,gcs
+ * The value is, e.g. file,smb,smb1,ftp,s3
* comment: Supported file protocols for crawling. * @return The value of found property. (NotNull: if not found, exception but basically no way) */ @@ -14560,7 +14560,7 @@ protected java.util.Map prepareGeneratedDefaultMap() { defaultMap.put(FessConfig.CRAWLER_DOCUMENT_FULLSTOP_CHARS, "u002eu06d4u2e3cu3002"); defaultMap.put(FessConfig.CRAWLER_CRAWLING_DATA_ENCODING, "UTF-8"); defaultMap.put(FessConfig.CRAWLER_WEB_PROTOCOLS, "http,https"); - defaultMap.put(FessConfig.CRAWLER_FILE_PROTOCOLS, "file,smb,smb1,ftp,s3,gcs"); + defaultMap.put(FessConfig.CRAWLER_FILE_PROTOCOLS, "file,smb,smb1,ftp,s3"); defaultMap.put(FessConfig.CRAWLER_DATA_ENV_PARAM_KEY_PATTERN, "^FESS_ENV_.*"); defaultMap.put(FessConfig.CRAWLER_IGNORE_ROBOTS_TXT, "false"); defaultMap.put(FessConfig.CRAWLER_IGNORE_ROBOTS_TAGS, "false"); diff --git a/src/main/java/org/codelibs/fess/storage/GcsStorageClient.java b/src/main/java/org/codelibs/fess/storage/GcsStorageClient.java deleted file mode 100644 index cf0c32068..000000000 --- a/src/main/java/org/codelibs/fess/storage/GcsStorageClient.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright 2012-2025 CodeLibs Project and the Others. - * - * Licensed 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.codelibs.fess.storage; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.codelibs.core.lang.StringUtil; -import org.codelibs.fess.crawler.Constants; -import org.codelibs.fess.exception.StorageException; -import org.codelibs.fess.mylasta.direction.FessConfig; -import org.codelibs.fess.util.ComponentUtil; - -import com.google.api.gax.paging.Page; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.NoCredentials; -import com.google.cloud.storage.Blob; -import com.google.cloud.storage.BlobId; -import com.google.cloud.storage.BlobInfo; -import com.google.cloud.storage.Bucket; -import com.google.cloud.storage.BucketInfo; -import com.google.cloud.storage.Storage; -import com.google.cloud.storage.Storage.BlobListOption; -import com.google.cloud.storage.StorageOptions; - -/** - * Google Cloud Storage client implementation. - */ -public class GcsStorageClient implements StorageClient { - - private static final Logger logger = LogManager.getLogger(GcsStorageClient.class); - - private static final String DEFAULT_GCS_HOST = "storage.googleapis.com"; - - private Storage storage; - private String bucket; - - /** - * Constructor for subclasses that customize behavior without initializing the GCS client. - * Subclasses using this constructor must override any method that depends on - * {@code storage} or {@code bucket}. - */ - protected GcsStorageClient() { - // configured by init(), or by a subclass that overrides everything using storage - } - - /** - * Creates a new GcsStorageClient instance. - * - * @param projectId the GCS project ID - * @param bucket the bucket name - * @param endpoint the custom endpoint URL (optional, for fake-gcs-server etc.) - * @param credentialsPath the path to the credentials JSON file (optional) - */ - public GcsStorageClient(final String projectId, final String bucket, final String endpoint, final String credentialsPath) { - configure(projectId, bucket, endpoint, credentialsPath); - } - - @Override - public void init() { - final FessConfig fessConfig = ComponentUtil.getFessConfig(); - configure(fessConfig.getStorageProjectId(), fessConfig.getStorageBucket(), fessConfig.getStorageEndpoint(), - fessConfig.getStorageCredentialsPath()); - } - - private void configure(final String projectId, final String bucket, final String endpoint, final String credentialsPath) { - this.bucket = bucket; - - final StorageOptions.Builder builder = StorageOptions.newBuilder(); - - if (StringUtil.isNotBlank(projectId)) { - builder.setProjectId(projectId); - } - - if (StringUtil.isNotBlank(endpoint) && !isDefaultEndpoint(endpoint)) { - // For fake-gcs-server or custom endpoint - builder.setHost(endpoint); - builder.setCredentials(NoCredentials.getInstance()); - if (logger.isDebugEnabled()) { - logger.debug("Using custom GCS endpoint: {}", endpoint); - } - } else // Production: use credentials file or default credentials - if (StringUtil.isNotBlank(credentialsPath)) { - try (FileInputStream fis = new FileInputStream(credentialsPath)) { - final GoogleCredentials credentials = GoogleCredentials.fromStream(fis); - builder.setCredentials(credentials); - } catch (final IOException e) { - throw new StorageException("Failed to load GCS credentials from " + credentialsPath, e); - } - } - // If no credentials path, uses default credentials (GOOGLE_APPLICATION_CREDENTIALS env var) - - this.storage = builder.build().getService(); - } - - /** - * Determines whether the given endpoint refers to the default GCS host - * ({@code storage.googleapis.com}). Subclasses may override this to - * recognize additional hosts as default. - * - * @param endpoint the endpoint to check (URL or host) - * @return {@code true} if the endpoint resolves to the default GCS host - */ - protected boolean isDefaultEndpoint(final String endpoint) { - if (StringUtil.isBlank(endpoint)) { - return false; - } - final String value = endpoint.trim(); - try { - final URI uri = URI.create(value.contains("://") ? value : "https://" + value); - return "https".equalsIgnoreCase(uri.getScheme()) && DEFAULT_GCS_HOST.equalsIgnoreCase(uri.getHost()) - && (uri.getPort() == -1 || uri.getPort() == 443); - } catch (final IllegalArgumentException e) { - return DEFAULT_GCS_HOST.equalsIgnoreCase(value); - } - } - - @Override - public void uploadObject(final String objectName, final InputStream inputStream, final long size, final String contentType) { - try { - final BlobId blobId = BlobId.of(bucket, objectName); - final BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType(contentType).build(); - storage.createFrom(blobInfo, inputStream); - } catch (final Exception e) { - throw new StorageException("Failed to upload " + objectName, e); - } - } - - @Override - public void downloadObject(final String objectName, final OutputStream outputStream) { - try { - final Blob blob = storage.get(BlobId.of(bucket, objectName)); - if (blob == null) { - throw new StorageException("Object not found: " + objectName); - } - blob.downloadTo(outputStream); - } catch (final StorageException e) { - throw e; - } catch (final Exception e) { - throw new StorageException("Failed to download " + objectName, e); - } - } - - @Override - public void deleteObject(final String objectName) { - try { - final boolean deleted = storage.delete(BlobId.of(bucket, objectName)); - if (!deleted && logger.isDebugEnabled()) { - logger.debug("Object may not exist: {}", objectName); - } - } catch (final Exception e) { - throw new StorageException("Failed to delete " + objectName, e); - } - } - - @Override - public List listObjects(final String prefix, final int maxItems) { - final List items = new ArrayList<>(); - final List fileItems = new ArrayList<>(); - - try { - final String searchPrefix = StringUtil.isNotBlank(prefix) ? (prefix.endsWith("/") ? prefix : prefix + "/") : ""; - - final Page blobs = storage.list(bucket, BlobListOption.prefix(searchPrefix), BlobListOption.currentDirectory(), - BlobListOption.pageSize(maxItems)); - - for (final Blob blob : blobs.iterateAll()) { - final String blobName = blob.getName(); - - // Skip the prefix itself - if (blobName.equals(searchPrefix)) { - continue; - } - - final boolean isDirectory = blobName.endsWith("/"); - final String name = getName(blobName); - - if (StringUtil.isBlank(name)) { - continue; - } - - final ZonedDateTime lastModified = - blob.getUpdateTimeOffsetDateTime() != null ? blob.getUpdateTimeOffsetDateTime().toZonedDateTime() : null; - - final StorageItem item = new StorageItem(name, prefix, isDirectory, isDirectory ? 0 : blob.getSize(), - isDirectory ? null : lastModified, encodeId(blobName)); - - if (isDirectory) { - items.add(item); - } else { - fileItems.add(item); - } - - if (items.size() + fileItems.size() >= maxItems) { - break; - } - } - } catch (final Exception e) { - if (logger.isDebugEnabled()) { - logger.debug("Failed to list objects in {}", bucket, e); - } - } - - items.addAll(fileItems); - return items; - } - - @Override - public Map getObjectTags(final String objectName) { - try { - final Blob blob = storage.get(BlobId.of(bucket, objectName)); - if (blob == null) { - return Collections.emptyMap(); - } - // GCS uses metadata instead of tags - final Map metadata = blob.getMetadata(); - return metadata != null ? new HashMap<>(metadata) : Collections.emptyMap(); - } catch (final Exception e) { - throw new StorageException("Failed to get tags from " + objectName, e); - } - } - - @Override - public void setObjectTags(final String objectName, final Map tags) { - try { - final Blob blob = storage.get(BlobId.of(bucket, objectName)); - if (blob == null) { - throw new StorageException("Object not found: " + objectName); - } - // GCS uses metadata instead of tags - blob.toBuilder().setMetadata(tags).build().update(); - } catch (final StorageException e) { - throw e; - } catch (final Exception e) { - throw new StorageException("Failed to update tags for " + objectName, e); - } - } - - @Override - public void ensureBucketExists() { - try { - final Bucket existingBucket = storage.get(bucket); - if (existingBucket == null) { - storage.create(BucketInfo.newBuilder(bucket).build()); - logger.info("Created storage bucket: {}", bucket); - } - } catch (final Exception e) { - logger.warn("Failed to ensure bucket exists: {}", bucket, e); - } - } - - @Override - public boolean isAvailable() { - try { - return storage.get(bucket) != null; - } catch (final Exception e) { - return false; - } - } - - @Override - public void close() { - // GCS Storage client doesn't require explicit close - // but we can try to close it if needed - if (storage == null) { - return; - } - try { - storage.close(); - } catch (final Exception e) { - if (logger.isDebugEnabled()) { - logger.debug("Failed to close GCS storage client", e); - } - } - } - - /** - * Extracts the file/directory name from a full object path. - * - * @param objectName the full object path - * @return the name portion of the path - */ - private String getName(final String objectName) { - if (StringUtil.isBlank(objectName)) { - return StringUtil.EMPTY; - } - // Remove trailing slash if present - String name = objectName; - if (name.endsWith("/")) { - name = name.substring(0, name.length() - 1); - } - final String[] values = name.split("/"); - if (values.length == 0) { - return StringUtil.EMPTY; - } - return values[values.length - 1]; - } - - /** - * Encodes an object name to a URL-safe base64 string. - * - * @param objectName the object name to encode - * @return base64 encoded string - */ - private String encodeId(final String objectName) { - if (objectName == null) { - return StringUtil.EMPTY; - } - return new String(Base64.getUrlEncoder().encode(objectName.getBytes(Constants.UTF_8_CHARSET)), Constants.UTF_8_CHARSET); - } -} diff --git a/src/main/resources/fess_config.properties b/src/main/resources/fess_config.properties index f1891f20b..930757415 100644 --- a/src/main/resources/fess_config.properties +++ b/src/main/resources/fess_config.properties @@ -383,7 +383,7 @@ crawler.crawling.data.encoding=UTF-8 # Supported web protocols for crawling. crawler.web.protocols=http,https # Supported file protocols for crawling. -crawler.file.protocols=file,smb,smb1,ftp,s3,gcs +crawler.file.protocols=file,smb,smb1,ftp,s3 # Pattern for environment variable keys in crawling data. crawler.data.env.param.key.pattern=^FESS_ENV_.* # Whether to ignore robots.txt during crawling. diff --git a/src/main/resources/fess_storage.xml b/src/main/resources/fess_storage.xml index 7ac0b426c..45abc7bcb 100644 --- a/src/main/resources/fess_storage.xml +++ b/src/main/resources/fess_storage.xml @@ -6,7 +6,9 @@ The mapping from a storage.type value to a class lives here rather than in Java so that the clients can ship as fess-lib-* plugins: a plugin contributes its component through fess_storage++.xml, which merges from every jar on the classpath, and setting - storage.type to its name is enough to reach it. + storage.type to its name is enough to reach it. fess-lib-gcs registers gcsStorageClient + that way; storage.type=gcs without that plugin installed is a configuration error the + factory reports rather than a missing class. They are prototypes, not singletons: every caller takes the client in a try-with-resources and closes it, which would shut down a shared instance. --> @@ -17,6 +19,4 @@ keeps the type-to-class mapping in one place. --> - - diff --git a/src/test/java/org/codelibs/fess/app/web/admin/wizard/AdminWizardActionTest.java b/src/test/java/org/codelibs/fess/app/web/admin/wizard/AdminWizardActionTest.java index 2b4dce88b..3c7568eb3 100644 --- a/src/test/java/org/codelibs/fess/app/web/admin/wizard/AdminWizardActionTest.java +++ b/src/test/java/org/codelibs/fess/app/web/admin/wizard/AdminWizardActionTest.java @@ -42,7 +42,7 @@ public String getCrawlerWebProtocols() { @Override public String getCrawlerFileProtocols() { - return "file,smb,smb1,ftp,s3,gcs"; + return "file,smb,smb1,ftp,s3"; } }); final ProtocolHelper protocolHelper = new ProtocolHelper(); diff --git a/src/test/java/org/codelibs/fess/app/web/go/GoActionTest.java b/src/test/java/org/codelibs/fess/app/web/go/GoActionTest.java index a14456b3c..97166ee3f 100644 --- a/src/test/java/org/codelibs/fess/app/web/go/GoActionTest.java +++ b/src/test/java/org/codelibs/fess/app/web/go/GoActionTest.java @@ -48,7 +48,7 @@ public String getCrawlerWebProtocols() { @Override public String getCrawlerFileProtocols() { - return "file,smb,smb1,ftp,s3,gcs"; + return "file,smb,smb1,ftp,s3"; } }); final ProtocolHelper protocolHelper = new ProtocolHelper(); diff --git a/src/test/java/org/codelibs/fess/crawler/transformer/FessFileTransformerTest.java b/src/test/java/org/codelibs/fess/crawler/transformer/FessFileTransformerTest.java index a291e981b..ae37d5ab2 100644 --- a/src/test/java/org/codelibs/fess/crawler/transformer/FessFileTransformerTest.java +++ b/src/test/java/org/codelibs/fess/crawler/transformer/FessFileTransformerTest.java @@ -46,7 +46,7 @@ public String getCrawlerWebProtocols() { @Override public String getCrawlerFileProtocols() { - return "file,smb,smb1,ftp,s3,gcs"; + return "file,smb,smb1,ftp,s3"; } @Override diff --git a/src/test/java/org/codelibs/fess/helper/ProtocolHelperTest.java b/src/test/java/org/codelibs/fess/helper/ProtocolHelperTest.java index 73c07d4e3..c86c02adc 100644 --- a/src/test/java/org/codelibs/fess/helper/ProtocolHelperTest.java +++ b/src/test/java/org/codelibs/fess/helper/ProtocolHelperTest.java @@ -738,14 +738,14 @@ public String getCrawlerWebProtocols() { @Override public String getCrawlerFileProtocols() { - return "file,smb,smb1,ftp,s3,gcs"; + return "file,smb,smb1,ftp,s3"; } }); final ProtocolHelper protocolHelper = new ProtocolHelper(); protocolHelper.init(); - assertEquals(6, protocolHelper.getFileProtocols().length); + assertEquals(5, protocolHelper.getFileProtocols().length); // All file protocols should be valid assertTrue(protocolHelper.isValidFileProtocol("file:///path/to/file")); @@ -753,7 +753,10 @@ public String getCrawlerFileProtocols() { assertTrue(protocolHelper.isValidFileProtocol("smb1://server/share")); assertTrue(protocolHelper.isValidFileProtocol("ftp://ftp.example.com/file")); assertTrue(protocolHelper.isValidFileProtocol("s3://bucket/key")); - assertTrue(protocolHelper.isValidFileProtocol("gcs://bucket/object")); + + // gcs is not in the shipped default: fess-lib-gcs adds it with addFileProtocol when the + // plugin is installed, which test_s3_gcs_protocols_add_dynamically covers. + assertFalse(protocolHelper.isValidFileProtocol("gcs://bucket/object")); // Web protocols should NOT be valid as file protocols assertFalse(protocolHelper.isValidFileProtocol("http://example.com")); diff --git a/src/test/java/org/codelibs/fess/storage/GcsStorageClientTest.java b/src/test/java/org/codelibs/fess/storage/GcsStorageClientTest.java deleted file mode 100644 index e590e332e..000000000 --- a/src/test/java/org/codelibs/fess/storage/GcsStorageClientTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2025 CodeLibs Project and the Others. - * - * Licensed 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.codelibs.fess.storage; - -import org.codelibs.fess.unit.UnitFessTestCase; -import org.junit.jupiter.api.Test; - -public class GcsStorageClientTest extends UnitFessTestCase { - - private static final class TestableGcsStorageClient extends GcsStorageClient { - TestableGcsStorageClient() { - super(); - } - } - - @Test - public void test_isDefaultEndpoint() { - try (GcsStorageClient client = new TestableGcsStorageClient()) { - assertTrue(client.isDefaultEndpoint("https://storage.googleapis.com")); - assertTrue(client.isDefaultEndpoint("https://storage.googleapis.com/")); - assertTrue(client.isDefaultEndpoint("storage.googleapis.com")); - assertTrue(client.isDefaultEndpoint("https://storage.googleapis.com/storage/v1")); - assertTrue(client.isDefaultEndpoint("https://storage.googleapis.com:443")); - - assertFalse(client.isDefaultEndpoint("")); - assertFalse(client.isDefaultEndpoint("http://storage.googleapis.com")); - assertFalse(client.isDefaultEndpoint("https://storage.googleapis.com:4443")); - assertFalse(client.isDefaultEndpoint("http://localhost:4443")); - assertFalse(client.isDefaultEndpoint("https://example.com/storage.googleapis.com")); - } - } -} diff --git a/src/test/java/org/codelibs/fess/storage/StorageClientFactoryTest.java b/src/test/java/org/codelibs/fess/storage/StorageClientFactoryTest.java index 2d037f4bb..7a8de6f03 100644 --- a/src/test/java/org/codelibs/fess/storage/StorageClientFactoryTest.java +++ b/src/test/java/org/codelibs/fess/storage/StorageClientFactoryTest.java @@ -49,19 +49,32 @@ public void test_componentName_detectsFromTheEndpoint() { } /** - * Every name {@link #test_componentName_detectsFromTheEndpoint} and the shipped - * storage.type values can produce has to exist in fess_storage.xml, or the admin storage + * Every name {@link #test_componentName_detectsFromTheEndpoint} and the storage.type values + * the distribution serves can produce has to exist in fess_storage.xml, or the admin storage * screen fails at runtime for a configuration the UI offers. */ @Test - public void test_everyShippedTypeHasAComponent() { - for (final StorageType type : StorageType.values()) { + public void test_everyTypeTheDistributionServesHasAComponent() { + for (final StorageType type : new StorageType[] { StorageType.S3, StorageType.S3_COMPAT }) { final String name = StorageClientFactory.componentName(type.name(), null); assertTrue(org.codelibs.fess.util.ComponentUtil.hasComponent(name), name + " is not registered in fess_storage.xml"); } assertTrue(org.codelibs.fess.util.ComponentUtil.hasComponent(StorageClientFactory.componentName("auto", null))); } + /** + * GCS is deliberately not in that list. Its client ships in fess-lib-gcs, together with the + * Google Cloud Storage SDK the distribution no longer carries, so what core keeps is the name + * the plugin registers under and the endpoint detection that produces it. Registering a + * component here again would pull the SDK back into the war. + */ + @Test + public void test_gcsIsServedByAPluginRatherThanCore() { + assertEquals("gcsStorageClient", StorageClientFactory.componentName(StorageType.GCS.name(), null)); + assertFalse(org.codelibs.fess.util.ComponentUtil.hasComponent("gcsStorageClient"), + "gcsStorageClient belongs to fess-lib-gcs, which contributes it through fess_storage++.xml"); + } + /** * A prototype, not a singleton: every caller closes the client it was handed, so a shared * instance would be shut down for everybody by the first caller to finish.