All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.storage.GrpcConversions Maven / Gradle / Ivy

There is a newer version: 2.45.0
Show newest version
/*
 * Copyright 2022 Google LLC
 *
 * 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 com.google.cloud.storage;

import static com.google.cloud.storage.Storage.BucketField.SOFT_DELETE_POLICY;
import static com.google.cloud.storage.Utils.bucketNameCodec;
import static com.google.cloud.storage.Utils.ifNonNull;
import static com.google.cloud.storage.Utils.lift;
import static com.google.cloud.storage.Utils.projectNameCodec;

import com.google.api.pathtemplate.PathTemplate;
import com.google.cloud.Binding;
import com.google.cloud.Condition;
import com.google.cloud.Policy;
import com.google.cloud.storage.Acl.Entity;
import com.google.cloud.storage.Acl.Role;
import com.google.cloud.storage.BlobInfo.CustomerEncryption;
import com.google.cloud.storage.BucketInfo.CustomPlacementConfig;
import com.google.cloud.storage.BucketInfo.LifecycleRule;
import com.google.cloud.storage.BucketInfo.LifecycleRule.AbortIncompleteMPUAction;
import com.google.cloud.storage.BucketInfo.Logging;
import com.google.cloud.storage.BucketInfo.PublicAccessPrevention;
import com.google.cloud.storage.Conversions.Codec;
import com.google.cloud.storage.HmacKey.HmacKeyState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.protobuf.ByteString;
import com.google.protobuf.ProtocolStringList;
import com.google.protobuf.Timestamp;
import com.google.storage.v2.Bucket;
import com.google.storage.v2.Bucket.Billing;
import com.google.storage.v2.Bucket.Website;
import com.google.storage.v2.BucketAccessControl;
import com.google.storage.v2.CryptoKeyName;
import com.google.storage.v2.HmacKeyMetadata;
import com.google.storage.v2.Object;
import com.google.storage.v2.ObjectAccessControl;
import com.google.storage.v2.ObjectChecksums;
import com.google.storage.v2.Owner;
import com.google.type.Date;
import com.google.type.Expr;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

final class GrpcConversions {
  static final GrpcConversions INSTANCE = new GrpcConversions();

  private final Codec entityCodec =
      Codec.of(this::entityEncode, this::entityDecode);
  private final Codec objectAclCodec =
      Codec.of(this::objectAclEncode, this::objectAclDecode);
  private final Codec bucketAclCodec =
      Codec.of(this::bucketAclEncode, this::bucketAclDecode);
  private final Codec hmacKeyMetadataCodec =
      Codec.of(this::hmacKeyMetadataEncode, this::hmacKeyMetadataDecode);
  private final Codec serviceAccountCodec =
      Codec.of(this::serviceAccountEncode, this::serviceAccountDecode);
  private final Codec corsCodec = Codec.of(this::corsEncode, this::corsDecode);
  private final Codec loggingCodec =
      Codec.of(this::loggingEncode, this::loggingDecode);
  private final Codec iamConfigurationCodec =
      Codec.of(this::iamConfigEncode, this::iamConfigDecode);
  private final Codec autoclassCodec =
      Codec.of(this::autoclassEncode, this::autoclassDecode);

  private final Codec softDeletePolicyCodec =
      Codec.of(this::softDeletePolicyEncode, this::softDeletePolicyDecode);
  private final Codec lifecycleRuleCodec =
      Codec.of(this::lifecycleRuleEncode, this::lifecycleRuleDecode);
  private final Codec bucketInfoCodec =
      Codec.of(this::bucketInfoEncode, this::bucketInfoDecode);
  private final Codec
      customerEncryptionCodec =
          Codec.of(this::customerEncryptionEncode, this::customerEncryptionDecode);
  private final Codec blobIdCodec =
      Codec.of(this::blobIdEncode, this::blobIdDecode);
  private final Codec blobInfoCodec =
      Codec.of(this::blobInfoEncode, this::blobInfoDecode);
  private final Codec policyCodec =
      Codec.of(this::policyEncode, this::policyDecode);
  private final Codec bindingCodec =
      Codec.of(this::bindingEncode, this::bindingDecode);
  private final Codec iamConditionCodec =
      Codec.of(this::conditionEncode, this::conditionDecode);

  private final Codec
      hierarchicalNamespaceCodec =
          Codec.of(this::hierarchicalNamespaceEncode, this::hierarchicalNamespaceDecode);

  private final Codec byteStringB64StringCodec =
      Codec.of(
          bs -> Base64.getEncoder().encodeToString(bs.toByteArray()),
          s -> ByteString.copyFrom(Base64.getDecoder().decode(s.getBytes(StandardCharsets.UTF_8))));

  @VisibleForTesting
  final Codec timestampCodec =
      Codec.of(
          odt ->
              Timestamp.newBuilder()
                  .setSeconds(odt.toEpochSecond())
                  .setNanos(odt.getNano())
                  .build(),
          t ->
              Instant.ofEpochSecond(t.getSeconds())
                  .plusNanos(t.getNanos())
                  .atOffset(ZoneOffset.UTC));

  @VisibleForTesting
  final Codec durationCodec =
      Codec.of(
          javaDuration ->
              com.google.protobuf.Duration.newBuilder()
                  .setSeconds(javaDuration.getSeconds())
                  .setNanos(javaDuration.getNano())
                  .build(),
          protoDuration ->
              Duration.ofSeconds(protoDuration.getSeconds()).plusNanos(protoDuration.getNanos()));

  @VisibleForTesting
  final Codec odtDateCodec =
      Codec.of(
          odt -> {
            OffsetDateTime utc = odt.withOffsetSameInstant(ZoneOffset.UTC);
            return Date.newBuilder()
                .setYear(utc.getYear())
                .setMonth(utc.getMonthValue())
                .setDay(utc.getDayOfMonth())
                .build();
          },
          d ->
              LocalDate.of(d.getYear(), d.getMonth(), d.getDay())
                  .atStartOfDay()
                  .atOffset(ZoneOffset.UTC));

  private GrpcConversions() {}

  Codec entity() {
    return entityCodec;
  }

  Codec objectAcl() {
    return objectAclCodec;
  }

  Codec bucketAcl() {
    return bucketAclCodec;
  }

  Codec hmacKeyMetadata() {
    return hmacKeyMetadataCodec;
  }

  Codec serviceAccount() {
    return serviceAccountCodec;
  }

  Codec cors() {
    return corsCodec;
  }

  Codec logging() {
    return loggingCodec;
  }

  Codec iamConfiguration() {
    return iamConfigurationCodec;
  }

  Codec lifecycleRule() {
    return lifecycleRuleCodec;
  }

  Codec bucketInfo() {
    return bucketInfoCodec;
  }

  Codec customerEncryption() {
    return customerEncryptionCodec;
  }

  Codec blobId() {
    return blobIdCodec;
  }

  Codec blobInfo() {
    return blobInfoCodec;
  }

  Codec policyCodec() {
    return policyCodec;
  }

  private BucketInfo bucketInfoDecode(Bucket from) {
    BucketInfo.Builder to = new BucketInfo.BuilderImpl(bucketNameCodec.decode(from.getName()));
    to.setProject(projectNameCodec.decode(from.getProject()));
    to.setGeneratedId(from.getBucketId());
    maybeDecodeRetentionPolicy(from, to);
    ifNonNull(from.getLocation(), to::setLocation);
    ifNonNull(from.getLocationType(), to::setLocationType);
    ifNonNull(from.getMetageneration(), to::setMetageneration);
    if (from.hasBilling()) {
      Billing billing = from.getBilling();
      to.setRequesterPays(billing.getRequesterPays());
    }
    if (from.hasCreateTime()) {
      to.setCreateTimeOffsetDateTime(timestampCodec.decode(from.getCreateTime()));
    }
    if (from.hasUpdateTime()) {
      to.setUpdateTimeOffsetDateTime(timestampCodec.decode(from.getUpdateTime()));
    }
    if (from.hasEncryption()) {
      to.setDefaultKmsKeyName(from.getEncryption().getDefaultKmsKey());
    }
    if (!from.getRpo().isEmpty()) {
      to.setRpo(Rpo.valueOf(from.getRpo()));
    }
    if (!from.getStorageClass().isEmpty()) {
      to.setStorageClass(StorageClass.valueOf(from.getStorageClass()));
    }
    if (from.hasVersioning()) {
      to.setVersioningEnabled(from.getVersioning().getEnabled());
    }
    ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
    Map labelsMap = from.getLabelsMap();
    if (!labelsMap.isEmpty()) {
      to.setLabels(labelsMap);
    }
    if (from.hasWebsite()) {
      Website website = from.getWebsite();
      String mainPageSuffix = website.getMainPageSuffix();
      if (!mainPageSuffix.isEmpty()) {
        to.setIndexPage(mainPageSuffix);
      }
      String notFoundPage = website.getNotFoundPage();
      if (!notFoundPage.isEmpty()) {
        to.setNotFoundPage(notFoundPage);
      }
    }
    if (from.hasLifecycle()) {
      to.setLifecycleRules(
          toImmutableListOf(lifecycleRuleCodec::decode).apply(from.getLifecycle().getRuleList()));
    }
    List corsList = from.getCorsList();
    if (!corsList.isEmpty()) {
      to.setCors(toImmutableListOf(corsCodec::decode).apply(corsList));
    }
    if (from.hasLogging()) {
      Bucket.Logging logging = from.getLogging();
      if (!logging.getLogBucket().isEmpty() || !logging.getLogObjectPrefix().isEmpty()) {
        to.setLogging(loggingCodec.decode(logging));
      }
    }
    if (from.hasOwner()) {
      to.setOwner(entityCodec.decode(from.getOwner().getEntity()));
    }

    List defaultObjectAclList = from.getDefaultObjectAclList();
    if (!defaultObjectAclList.isEmpty()) {
      to.setDefaultAcl(toImmutableListOf(objectAclCodec::decode).apply(defaultObjectAclList));
    }
    List bucketAclList = from.getAclList();
    if (!bucketAclList.isEmpty()) {
      to.setAcl(toImmutableListOf(bucketAclCodec::decode).apply(bucketAclList));
    }
    if (from.hasIamConfig()) {
      to.setIamConfiguration(iamConfigurationCodec.decode(from.getIamConfig()));
    }
    if (from.hasAutoclass()) {
      to.setAutoclass(autoclassCodec.decode(from.getAutoclass()));
    }
    if (from.hasSoftDeletePolicy()) {
      to.setSoftDeletePolicy(softDeletePolicyCodec.decode(from.getSoftDeletePolicy()));
    }
    if (from.hasCustomPlacementConfig()) {
      Bucket.CustomPlacementConfig customPlacementConfig = from.getCustomPlacementConfig();
      to.setCustomPlacementConfig(
          CustomPlacementConfig.newBuilder()
              .setDataLocations(customPlacementConfig.getDataLocationsList())
              .build());
    }
    if (from.hasHierarchicalNamespace()) {
      to.setHierarchicalNamespace(
          hierarchicalNamespaceCodec.decode(from.getHierarchicalNamespace()));
    }
    // TODO(frankyn): Add SelfLink when the field is available
    if (!from.getEtag().isEmpty()) {
      to.setEtag(from.getEtag());
    }
    return to.build();
  }

  private Bucket bucketInfoEncode(BucketInfo from) {
    Bucket.Builder to = Bucket.newBuilder();
    to.setName(bucketNameCodec.encode(from.getName()));
    ifNonNull(from.getProject(), projectNameCodec::encode, to::setProject);
    ifNonNull(from.getGeneratedId(), to::setBucketId);
    maybeEncodeRetentionPolicy(from, to);
    ifNonNull(from.getLocation(), to::setLocation);
    ifNonNull(from.getLocationType(), to::setLocationType);
    ifNonNull(from.getMetageneration(), to::setMetageneration);
    if (from.requesterPays() != null) {
      Bucket.Billing.Builder billingBuilder = Billing.newBuilder();
      ifNonNull(from.requesterPays(), billingBuilder::setRequesterPays);
      to.setBilling(billingBuilder.build());
    }
    ifNonNull(from.getCreateTimeOffsetDateTime(), timestampCodec::encode, to::setCreateTime);
    ifNonNull(from.getUpdateTimeOffsetDateTime(), timestampCodec::encode, to::setUpdateTime);
    if (from.getDefaultKmsKeyName() != null) {
      Bucket.Encryption.Builder encryptionBuilder = Bucket.Encryption.newBuilder();
      ifNonNull(from.getDefaultKmsKeyName(), encryptionBuilder::setDefaultKmsKey);
      to.setEncryption(encryptionBuilder.build());
    }
    if (from.getIndexPage() != null || from.getNotFoundPage() != null) {
      Bucket.Website.Builder websiteBuilder = Bucket.Website.newBuilder();
      ifNonNull(from.getIndexPage(), websiteBuilder::setMainPageSuffix);
      ifNonNull(from.getNotFoundPage(), websiteBuilder::setNotFoundPage);
      to.setWebsite(websiteBuilder.build());
    }
    ifNonNull(from.getRpo(), Rpo::toString, to::setRpo);
    ifNonNull(from.getStorageClass(), StorageClass::toString, to::setStorageClass);
    if (from.versioningEnabled() != null) {
      Bucket.Versioning.Builder versioningBuilder = Bucket.Versioning.newBuilder();
      ifNonNull(from.versioningEnabled(), versioningBuilder::setEnabled);
      to.setVersioning(versioningBuilder.build());
    }
    ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
    ifNonNull(from.getLabels(), this::removeNullValues, to::putAllLabels);
    // Do not use, #getLifecycleRules, it can not return null, which is important to our logic here
    List lifecycleRules = from.lifecycleRules;
    if (lifecycleRules != null) {
      Bucket.Lifecycle.Builder lifecycleBuilder = Bucket.Lifecycle.newBuilder();
      if (!lifecycleRules.isEmpty()) {
        ImmutableSet set =
            from.getLifecycleRules().stream()
                .map(lifecycleRuleCodec::encode)
                .collect(ImmutableSet.toImmutableSet());
        lifecycleBuilder.addAllRule(ImmutableList.copyOf(set));
      }
      to.setLifecycle(lifecycleBuilder.build());
    }

    Logging logging = from.getLogging();
    if (logging != null) {
      // an empty bucket name is invalid, don't even attempt to encode if neither name or prefix
      // are both empty
      if ((logging.getLogBucket() != null && !logging.getLogBucket().isEmpty())
          || (logging.getLogObjectPrefix() != null && !logging.getLogObjectPrefix().isEmpty())) {
        to.setLogging(loggingCodec.encode(logging));
      }
    }
    ifNonNull(from.getCors(), toImmutableListOf(corsCodec::encode), to::addAllCors);
    ifNonNull(
        from.getOwner(),
        lift(entity()::encode).andThen(o -> Owner.newBuilder().setEntity(o).build()),
        to::setOwner);
    ifNonNull(
        from.getDefaultAcl(),
        toImmutableListOf(objectAclCodec::encode),
        to::addAllDefaultObjectAcl);
    ifNonNull(from.getAcl(), toImmutableListOf(bucketAclCodec::encode), to::addAllAcl);
    ifNonNull(from.getIamConfiguration(), iamConfigurationCodec::encode, to::setIamConfig);
    ifNonNull(from.getAutoclass(), autoclassCodec::encode, to::setAutoclass);
    ifNonNull(from.getSoftDeletePolicy(), softDeletePolicyCodec::encode, to::setSoftDeletePolicy);
    if (from.getModifiedFields().contains(SOFT_DELETE_POLICY)
        && from.getSoftDeletePolicy() == null) {
      to.clearSoftDeletePolicy();
    }
    CustomPlacementConfig customPlacementConfig = from.getCustomPlacementConfig();
    if (customPlacementConfig != null && customPlacementConfig.getDataLocations() != null) {
      to.setCustomPlacementConfig(
          Bucket.CustomPlacementConfig.newBuilder()
              .addAllDataLocations(customPlacementConfig.getDataLocations())
              .build());
    }
    ifNonNull(
        from.getHierarchicalNamespace(),
        hierarchicalNamespaceCodec::encode,
        to::setHierarchicalNamespace);
    // TODO(frankyn): Add SelfLink when the field is available
    ifNonNull(from.getEtag(), to::setEtag);
    return to.build();
  }

  private void maybeEncodeRetentionPolicy(BucketInfo from, Bucket.Builder to) {
    if (from.getRetentionPeriodDuration() != null
        || from.retentionPolicyIsLocked() != null
        || from.getRetentionEffectiveTimeOffsetDateTime() != null) {
      Bucket.RetentionPolicy.Builder retentionPolicyBuilder = to.getRetentionPolicyBuilder();
      ifNonNull(
          from.getRetentionPeriodDuration(),
          durationCodec::encode,
          retentionPolicyBuilder::setRetentionDuration);
      ifNonNull(from.retentionPolicyIsLocked(), retentionPolicyBuilder::setIsLocked);
      ifNonNull(
          from.getRetentionEffectiveTimeOffsetDateTime(),
          timestampCodec::encode,
          retentionPolicyBuilder::setEffectiveTime);
      to.setRetentionPolicy(retentionPolicyBuilder.build());
    }
  }

  private void maybeDecodeRetentionPolicy(Bucket from, BucketInfo.Builder to) {
    if (from.hasRetentionPolicy()) {
      Bucket.RetentionPolicy retentionPolicy = from.getRetentionPolicy();
      to.setRetentionPolicyIsLocked(retentionPolicy.getIsLocked());
      if (retentionPolicy.hasRetentionDuration()) {
        to.setRetentionPeriodDuration(durationCodec.decode(retentionPolicy.getRetentionDuration()));
      }
      if (retentionPolicy.hasEffectiveTime()) {
        to.setRetentionEffectiveTimeOffsetDateTime(
            timestampCodec.decode(retentionPolicy.getEffectiveTime()));
      }
    }
  }

  private Bucket.Logging loggingEncode(BucketInfo.Logging from) {
    Bucket.Logging.Builder to = Bucket.Logging.newBuilder();
    if (from.getLogObjectPrefix() != null && !from.getLogObjectPrefix().isEmpty()) {
      to.setLogObjectPrefix(from.getLogObjectPrefix());
    }
    ifNonNull(from.getLogBucket(), bucketNameCodec::encode, to::setLogBucket);
    return to.build();
  }

  private BucketInfo.Logging loggingDecode(Bucket.Logging from) {
    BucketInfo.Logging.Builder to = BucketInfo.Logging.newBuilder();
    String logObjectPrefix = from.getLogObjectPrefix();
    if (!logObjectPrefix.isEmpty()) {
      to.setLogObjectPrefix(logObjectPrefix);
    }
    String logBucket = from.getLogBucket();
    if (!logBucket.isEmpty()) {
      to.setLogBucket(bucketNameCodec.decode(logBucket));
    }
    return to.build();
  }

  private Bucket.Cors corsEncode(Cors from) {
    Bucket.Cors.Builder to = Bucket.Cors.newBuilder();
    to.setMaxAgeSeconds(from.getMaxAgeSeconds());
    to.addAllResponseHeader(from.getResponseHeaders());
    ifNonNull(from.getMethods(), toImmutableListOf(java.lang.Object::toString), to::addAllMethod);
    ifNonNull(from.getOrigins(), toImmutableListOf(java.lang.Object::toString), to::addAllOrigin);
    return to.build();
  }

  private Cors corsDecode(Bucket.Cors from) {
    Cors.Builder to = Cors.newBuilder().setMaxAgeSeconds(from.getMaxAgeSeconds());
    ifNonNull(
        from.getMethodList(),
        m ->
            m.stream()
                .map(String::toUpperCase)
                .map(HttpMethod::valueOf)
                .collect(ImmutableList.toImmutableList()),
        to::setMethods);
    ifNonNull(from.getOriginList(), toImmutableListOf(Cors.Origin::of), to::setOrigins);
    to.setResponseHeaders(from.getResponseHeaderList());
    return to.build();
  }

  private String entityEncode(Acl.Entity from) {
    if (from instanceof Acl.RawEntity) {
      return from.getValue();
    } else if (from instanceof Acl.User) {
      switch (from.getValue()) {
        case Acl.User.ALL_AUTHENTICATED_USERS:
          return Acl.User.ALL_AUTHENTICATED_USERS;
        case Acl.User.ALL_USERS:
          return Acl.User.ALL_USERS;
        default:
          break;
      }
    }
    // intentionally not an else so that if the default is hit above it will fall through to here
    return from.getType().name().toLowerCase() + "-" + from.getValue();
  }

  private Acl.Entity entityDecode(String from) {
    if (from.startsWith("user-")) {
      return new Acl.User(from.substring(5));
    }
    if (from.equals(Acl.User.ALL_USERS)) {
      return Acl.User.ofAllUsers();
    }
    if (from.equals(Acl.User.ALL_AUTHENTICATED_USERS)) {
      return Acl.User.ofAllAuthenticatedUsers();
    }
    if (from.startsWith("group-")) {
      return new Acl.Group(from.substring(6));
    }
    if (from.startsWith("domain-")) {
      return new Acl.Domain(from.substring(7));
    }
    if (from.startsWith("project-")) {
      int idx = from.indexOf('-', 8);
      String team = from.substring(8, idx);
      String projectId = from.substring(idx + 1);
      return new Acl.Project(Acl.Project.ProjectRole.valueOf(team.toUpperCase()), projectId);
    }
    return new Acl.RawEntity(from);
  }

  private Acl objectAclDecode(ObjectAccessControl from) {
    Acl.Role role = Acl.Role.valueOf(from.getRole());
    Acl.Entity entity = entityCodec.decode(from.getEntity());
    Acl.Builder to = Acl.newBuilder(entity, role);
    if (!from.getId().isEmpty()) {
      to.setId(from.getId());
    }
    if (!from.getEtag().isEmpty()) {
      to.setEtag(from.getEtag());
    }
    return to.build();
  }

  private ObjectAccessControl objectAclEncode(Acl from) {
    ObjectAccessControl.Builder to = ObjectAccessControl.newBuilder();
    ifNonNull(from.getEntity(), entityCodec::encode, to::setEntity);
    ifNonNull(from.getRole(), Role::name, to::setRole);
    ifNonNull(from.getId(), to::setId);
    ifNonNull(from.getEtag(), to::setEtag);
    return to.build();
  }

  private Acl bucketAclDecode(com.google.storage.v2.BucketAccessControl from) {
    Role role = Role.valueOf(from.getRole());
    Entity entity = entityDecode(from.getEntity());
    Acl.Builder to = Acl.newBuilder(entity, role).setId(from.getId());
    if (!from.getEtag().isEmpty()) {
      to.setEtag(from.getEtag());
    }
    return to.build();
  }

  private com.google.storage.v2.BucketAccessControl bucketAclEncode(Acl from) {
    BucketAccessControl.Builder to = BucketAccessControl.newBuilder();
    ifNonNull(from.getEntity(), entityCodec::encode, to::setEntity);
    ifNonNull(from.getRole(), Role::toString, to::setRole);
    ifNonNull(from.getId(), to::setId);
    ifNonNull(from.getEtag(), to::setEtag);
    return to.build();
  }

  private Bucket.IamConfig.UniformBucketLevelAccess ublaEncode(BucketInfo.IamConfiguration from) {
    Bucket.IamConfig.UniformBucketLevelAccess.Builder to =
        Bucket.IamConfig.UniformBucketLevelAccess.newBuilder();
    ifNonNull(from.isUniformBucketLevelAccessEnabled(), to::setEnabled);
    if (from.isUniformBucketLevelAccessEnabled() == Boolean.TRUE) {
      ifNonNull(
          from.getUniformBucketLevelAccessLockedTimeOffsetDateTime(),
          timestampCodec::encode,
          to::setLockTime);
    }
    return to.build();
  }

  private BucketInfo.Autoclass autoclassDecode(Bucket.Autoclass from) {
    BucketInfo.Autoclass.Builder to = BucketInfo.Autoclass.newBuilder();
    to.setEnabled(from.getEnabled());
    ifNonNull(from.getToggleTime(), timestampCodec::decode, to::setToggleTime);

    String terminalStorageClass = from.getTerminalStorageClass();
    if (!terminalStorageClass.isEmpty()) {
      to.setTerminalStorageClass(StorageClass.valueOf(terminalStorageClass));
    }
    ifNonNull(
        from.getTerminalStorageClassUpdateTime(),
        timestampCodec::decode,
        to::setTerminalStorageClassUpdateTime);
    return to.build();
  }

  private Bucket.Autoclass autoclassEncode(BucketInfo.Autoclass from) {
    Bucket.Autoclass.Builder to = Bucket.Autoclass.newBuilder();
    ifNonNull(from.getEnabled(), to::setEnabled);
    ifNonNull(from.getToggleTime(), timestampCodec::encode, to::setToggleTime);
    ifNonNull(from.getTerminalStorageClass(), StorageClass::toString, to::setTerminalStorageClass);
    ifNonNull(
        from.getTerminalStorageClassUpdateTime(),
        timestampCodec::encode,
        to::setTerminalStorageClassUpdateTime);
    return to.build();
  }

  private BucketInfo.SoftDeletePolicy softDeletePolicyDecode(Bucket.SoftDeletePolicy from) {
    BucketInfo.SoftDeletePolicy.Builder to = BucketInfo.SoftDeletePolicy.newBuilder();
    ifNonNull(from.getRetentionDuration(), durationCodec::decode, to::setRetentionDuration);
    ifNonNull(from.getEffectiveTime(), timestampCodec::decode, to::setEffectiveTime);
    return to.build();
  }

  private Bucket.SoftDeletePolicy softDeletePolicyEncode(BucketInfo.SoftDeletePolicy from) {
    Bucket.SoftDeletePolicy.Builder to = Bucket.SoftDeletePolicy.newBuilder();
    ifNonNull(from.getRetentionDuration(), durationCodec::encode, to::setRetentionDuration);
    return to.build();
  }

  private Bucket.HierarchicalNamespace hierarchicalNamespaceEncode(
      BucketInfo.HierarchicalNamespace from) {
    Bucket.HierarchicalNamespace.Builder to = Bucket.HierarchicalNamespace.newBuilder();
    ifNonNull(from.getEnabled(), to::setEnabled);
    return to.build();
  }

  private BucketInfo.HierarchicalNamespace hierarchicalNamespaceDecode(
      Bucket.HierarchicalNamespace from) {
    BucketInfo.HierarchicalNamespace.Builder to = BucketInfo.HierarchicalNamespace.newBuilder();
    to.setEnabled(from.getEnabled());
    return to.build();
  }

  private Bucket.IamConfig iamConfigEncode(BucketInfo.IamConfiguration from) {
    Bucket.IamConfig.Builder to = Bucket.IamConfig.newBuilder();
    to.setUniformBucketLevelAccess(ublaEncode(from));
    if (from.getPublicAccessPrevention() != null) {
      ifNonNull(from.getPublicAccessPrevention().getValue(), to::setPublicAccessPrevention);
    }
    return to.build();
  }

  private BucketInfo.IamConfiguration iamConfigDecode(Bucket.IamConfig from) {
    Bucket.IamConfig.UniformBucketLevelAccess ubla = from.getUniformBucketLevelAccess();

    BucketInfo.IamConfiguration.Builder to = BucketInfo.IamConfiguration.newBuilder();
    ifNonNull(ubla.getEnabled(), to::setIsUniformBucketLevelAccessEnabled);
    if (ubla.hasLockTime()) {
      to.setUniformBucketLevelAccessLockedTimeOffsetDateTime(
          timestampCodec.decode(ubla.getLockTime()));
    }
    if (!from.getPublicAccessPrevention().isEmpty()) {
      to.setPublicAccessPrevention(PublicAccessPrevention.parse(from.getPublicAccessPrevention()));
    }
    return to.build();
  }

  private Bucket.Lifecycle.Rule lifecycleRuleEncode(BucketInfo.LifecycleRule from) {
    Bucket.Lifecycle.Rule.Builder to = Bucket.Lifecycle.Rule.newBuilder();
    to.setAction(ruleActionEncode(from.getAction()));
    to.setCondition(ruleConditionEncode(from.getCondition()));
    return to.build();
  }

  private Bucket.Lifecycle.Rule.Condition ruleConditionEncode(
      BucketInfo.LifecycleRule.LifecycleCondition from) {
    Bucket.Lifecycle.Rule.Condition.Builder to = Bucket.Lifecycle.Rule.Condition.newBuilder();
    if (from.getAge() != null) {
      to.setAgeDays(from.getAge());
    }
    if (from.getIsLive() != null) {
      to.setIsLive(from.getIsLive());
    }
    if (from.getNumberOfNewerVersions() != null) {
      to.setNumNewerVersions(from.getNumberOfNewerVersions());
    }
    if (from.getDaysSinceNoncurrentTime() != null) {
      to.setDaysSinceNoncurrentTime(from.getDaysSinceNoncurrentTime());
    }
    if (from.getDaysSinceCustomTime() != null) {
      to.setDaysSinceCustomTime(from.getDaysSinceCustomTime());
    }
    ifNonNull(from.getCreatedBeforeOffsetDateTime(), odtDateCodec::encode, to::setCreatedBefore);
    ifNonNull(
        from.getNoncurrentTimeBeforeOffsetDateTime(),
        odtDateCodec::encode,
        to::setNoncurrentTimeBefore);
    ifNonNull(
        from.getCustomTimeBeforeOffsetDateTime(), odtDateCodec::encode, to::setCustomTimeBefore);
    ifNonNull(
        from.getMatchesStorageClass(),
        toImmutableListOf(StorageClass::toString),
        to::addAllMatchesStorageClass);
    ifNonNull(from.getMatchesPrefix(), to::addAllMatchesPrefix);
    ifNonNull(from.getMatchesSuffix(), to::addAllMatchesSuffix);
    return to.build();
  }

  private Bucket.Lifecycle.Rule.Action ruleActionEncode(
      BucketInfo.LifecycleRule.LifecycleAction from) {
    Bucket.Lifecycle.Rule.Action.Builder to =
        Bucket.Lifecycle.Rule.Action.newBuilder().setType(from.getActionType());
    if (from.getActionType().equals(BucketInfo.LifecycleRule.SetStorageClassLifecycleAction.TYPE)) {
      to.setStorageClass(
          ((BucketInfo.LifecycleRule.SetStorageClassLifecycleAction) from)
              .getStorageClass()
              .toString());
    }
    return to.build();
  }

  private BucketInfo.LifecycleRule lifecycleRuleDecode(Bucket.Lifecycle.Rule from) {
    BucketInfo.LifecycleRule.LifecycleAction lifecycleAction;

    Bucket.Lifecycle.Rule.Action action = from.getAction();

    switch (action.getType()) {
      case BucketInfo.LifecycleRule.DeleteLifecycleAction.TYPE:
        lifecycleAction = BucketInfo.LifecycleRule.LifecycleAction.newDeleteAction();
        break;
      case BucketInfo.LifecycleRule.SetStorageClassLifecycleAction.TYPE:
        lifecycleAction =
            BucketInfo.LifecycleRule.LifecycleAction.newSetStorageClassAction(
                StorageClass.valueOf(action.getStorageClass()));
        break;
      case AbortIncompleteMPUAction.TYPE:
        lifecycleAction =
            BucketInfo.LifecycleRule.LifecycleAction.newAbortIncompleteMPUploadAction();
        break;
      default:
        BucketInfo.log.warning(
            "The lifecycle action "
                + action.getType()
                + " is not supported by this version of the library. "
                + "Attempting to update with this rule may cause errors. Please "
                + "update to the latest version of google-cloud-storage.");
        lifecycleAction =
            BucketInfo.LifecycleRule.LifecycleAction.newLifecycleAction("Unknown action");
    }

    Bucket.Lifecycle.Rule.Condition condition = from.getCondition();

    BucketInfo.LifecycleRule.LifecycleCondition.Builder conditionBuilder =
        BucketInfo.LifecycleRule.LifecycleCondition.newBuilder();
    if (condition.hasAgeDays()) {
      conditionBuilder.setAge(condition.getAgeDays());
    }
    if (condition.hasCreatedBefore()) {
      conditionBuilder.setCreatedBeforeOffsetDateTime(
          odtDateCodec.nullable().decode(condition.getCreatedBefore()));
    }
    if (condition.hasIsLive()) {
      conditionBuilder.setIsLive(condition.getIsLive());
    }
    if (condition.hasNumNewerVersions()) {
      conditionBuilder.setNumberOfNewerVersions(condition.getNumNewerVersions());
    }
    if (condition.hasDaysSinceNoncurrentTime()) {
      conditionBuilder.setDaysSinceNoncurrentTime(condition.getDaysSinceNoncurrentTime());
    }
    if (condition.hasNoncurrentTimeBefore()) {
      conditionBuilder.setNoncurrentTimeBeforeOffsetDateTime(
          odtDateCodec.decode(condition.getNoncurrentTimeBefore()));
    }
    if (condition.hasCustomTimeBefore()) {
      conditionBuilder.setCustomTimeBeforeOffsetDateTime(
          odtDateCodec.decode(condition.getCustomTimeBefore()));
    }
    if (condition.hasDaysSinceCustomTime()) {
      conditionBuilder.setDaysSinceCustomTime(condition.getDaysSinceCustomTime());
    }
    if (!condition.getMatchesStorageClassList().isEmpty()) {
      ImmutableList collect =
          condition.getMatchesStorageClassList().stream()
              .map(StorageClass::valueOf)
              .collect(ImmutableList.toImmutableList());
      conditionBuilder.setMatchesStorageClass(collect);
    }
    if (!condition.getMatchesPrefixList().isEmpty()) {
      conditionBuilder.setMatchesPrefix(condition.getMatchesPrefixList());
    }
    if (!condition.getMatchesSuffixList().isEmpty()) {
      conditionBuilder.setMatchesSuffix(condition.getMatchesSuffixList());
    }
    return new BucketInfo.LifecycleRule(lifecycleAction, conditionBuilder.build());
  }

  private HmacKeyMetadata hmacKeyMetadataEncode(HmacKey.HmacKeyMetadata from) {
    HmacKeyMetadata.Builder to = HmacKeyMetadata.newBuilder();
    ifNonNull(from.getEtag(), to::setEtag);
    ifNonNull(from.getId(), to::setId);
    ifNonNull(from.getAccessId(), to::setAccessId);
    ifNonNull(from.getProjectId(), projectNameCodec::encode, to::setProject);
    ifNonNull(from.getServiceAccount(), ServiceAccount::getEmail, to::setServiceAccountEmail);
    ifNonNull(from.getState(), Enum::name, to::setState);
    ifNonNull(from.getCreateTimeOffsetDateTime(), timestampCodec::encode, to::setCreateTime);
    ifNonNull(from.getUpdateTimeOffsetDateTime(), timestampCodec::encode, to::setUpdateTime);
    return to.build();
  }

  private HmacKey.HmacKeyMetadata hmacKeyMetadataDecode(HmacKeyMetadata from) {
    HmacKey.HmacKeyMetadata.Builder to =
        HmacKey.HmacKeyMetadata.newBuilder(ServiceAccount.of(from.getServiceAccountEmail()))
            .setAccessId(from.getAccessId())
            .setCreateTimeOffsetDateTime(timestampCodec.decode(from.getCreateTime()))
            .setId(from.getId())
            .setProjectId(projectNameCodec.decode(from.getProject()))
            .setState(HmacKeyState.valueOf(from.getState()))
            .setUpdateTimeOffsetDateTime(timestampCodec.decode(from.getUpdateTime()));
    if (!from.getEtag().isEmpty()) {
      to.setEtag(from.getEtag());
    }
    return to.build();
  }

  private com.google.storage.v2.ServiceAccount serviceAccountEncode(ServiceAccount from) {
    return com.google.storage.v2.ServiceAccount.newBuilder()
        .setEmailAddress(from.getEmail())
        .build();
  }

  private ServiceAccount serviceAccountDecode(com.google.storage.v2.ServiceAccount from) {
    return ServiceAccount.of(from.getEmailAddress());
  }

  private com.google.storage.v2.CustomerEncryption customerEncryptionEncode(
      CustomerEncryption from) {
    return com.google.storage.v2.CustomerEncryption.newBuilder()
        .setEncryptionAlgorithm(from.getEncryptionAlgorithm())
        .setKeySha256Bytes(ByteString.copyFrom(BaseEncoding.base64().decode(from.getKeySha256())))
        .build();
  }

  private CustomerEncryption customerEncryptionDecode(
      com.google.storage.v2.CustomerEncryption from) {
    return new CustomerEncryption(
        from.getEncryptionAlgorithm(),
        BaseEncoding.base64().encode(from.getKeySha256Bytes().toByteArray()));
  }

  private Object blobIdEncode(BlobId from) {
    Object.Builder to = Object.newBuilder();
    ifNonNull(from.getBucket(), bucketNameCodec::encode, to::setBucket);
    ifNonNull(from.getName(), to::setName);
    ifNonNull(from.getGeneration(), to::setGeneration);
    return to.build();
  }

  private BlobId blobIdDecode(Object from) {
    String bucketName = bucketNameCodec.decode(from.getBucket());
    return BlobId.of(bucketName, from.getName(), from.getGeneration());
  }

  private Object blobInfoEncode(BlobInfo from) {
    Object.Builder toBuilder = Object.newBuilder();
    ifNonNull(from.getBucket(), bucketNameCodec::encode, toBuilder::setBucket);
    ifNonNull(from.getName(), toBuilder::setName);
    ifNonNull(from.getGeneration(), toBuilder::setGeneration);
    ifNonNull(from.getCacheControl(), toBuilder::setCacheControl);
    ifNonNull(from.getSize(), toBuilder::setSize);
    ifNonNull(from.getContentType(), toBuilder::setContentType);
    ifNonNull(from.getContentEncoding(), toBuilder::setContentEncoding);
    ifNonNull(from.getContentDisposition(), toBuilder::setContentDisposition);
    ifNonNull(from.getContentLanguage(), toBuilder::setContentLanguage);
    ifNonNull(from.getComponentCount(), toBuilder::setComponentCount);
    if (from.getMd5() != null || from.getCrc32c() != null) {
      ObjectChecksums.Builder objectChecksums = ObjectChecksums.newBuilder();
      if (from.getMd5() != null) {
        objectChecksums.setMd5Hash(
            ByteString.copyFrom(BaseEncoding.base64().decode(from.getMd5())));
      }
      if (from.getCrc32c() != null) {
        objectChecksums.setCrc32C(Utils.crc32cCodec.decode(from.getCrc32c()));
      }
      toBuilder.setChecksums(objectChecksums.build());
    }
    ifNonNull(from.getMetageneration(), toBuilder::setMetageneration);
    ifNonNull(from.getDeleteTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setDeleteTime);
    ifNonNull(from.getUpdateTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setUpdateTime);
    ifNonNull(from.getCreateTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setCreateTime);
    ifNonNull(from.getCustomTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setCustomTime);
    ifNonNull(from.getSoftDeleteTime(), timestampCodec::encode, toBuilder::setSoftDeleteTime);
    ifNonNull(from.getHardDeleteTime(), timestampCodec::encode, toBuilder::setHardDeleteTime);
    ifNonNull(
        from.getCustomerEncryption(),
        customerEncryptionCodec::encode,
        toBuilder::setCustomerEncryption);
    ifNonNull(from.getStorageClass(), StorageClass::toString, toBuilder::setStorageClass);
    ifNonNull(
        from.getTimeStorageClassUpdatedOffsetDateTime(),
        timestampCodec::encode,
        toBuilder::setUpdateStorageClassTime);
    ifNonNull(from.getKmsKeyName(), this::removeKmsVersion, toBuilder::setKmsKey);
    ifNonNull(from.getEventBasedHold(), toBuilder::setEventBasedHold);
    ifNonNull(from.getTemporaryHold(), toBuilder::setTemporaryHold);
    ifNonNull(
        from.getRetentionExpirationTimeOffsetDateTime(),
        timestampCodec::encode,
        toBuilder::setRetentionExpireTime);
    // TODO(sydmunro): Add Selflink when available
    ifNonNull(from.getEtag(), toBuilder::setEtag);
    Entity entity = from.getOwner();
    if (entity != null) {
      toBuilder.setOwner(Owner.newBuilder().setEntity(entityEncode(entity)).build());
    }
    ifNonNull(from.getMetadata(), this::removeNullValues, toBuilder::putAllMetadata);
    ifNonNull(from.getAcl(), toImmutableListOf(objectAcl()::encode), toBuilder::addAllAcl);
    return toBuilder.build();
  }

  private BlobInfo blobInfoDecode(Object from) {
    BlobInfo.Builder toBuilder =
        BlobInfo.newBuilder(
            BlobId.of(
                bucketNameCodec.decode(from.getBucket()), from.getName(), from.getGeneration()));
    ifNonNull(from.getCacheControl(), toBuilder::setCacheControl);
    ifNonNull(from.getSize(), toBuilder::setSize);
    ifNonNull(from.getContentType(), toBuilder::setContentType);
    ifNonNull(from.getContentEncoding(), toBuilder::setContentEncoding);
    ifNonNull(from.getContentDisposition(), toBuilder::setContentDisposition);
    ifNonNull(from.getContentLanguage(), toBuilder::setContentLanguage);
    ifNonNull(from.getComponentCount(), toBuilder::setComponentCount);
    if (from.hasChecksums()) {
      ObjectChecksums checksums = from.getChecksums();
      if (checksums.hasCrc32C()) {
        toBuilder.setCrc32c(Utils.crc32cCodec.encode(checksums.getCrc32C()));
      }
      ByteString md5Hash = checksums.getMd5Hash();
      if (!md5Hash.isEmpty()) {
        toBuilder.setMd5(BaseEncoding.base64().encode(md5Hash.toByteArray()));
      }
    }
    ifNonNull(from.getMetageneration(), toBuilder::setMetageneration);
    if (from.hasDeleteTime()) {
      toBuilder.setDeleteTimeOffsetDateTime(timestampCodec.decode(from.getDeleteTime()));
    }
    if (from.hasUpdateTime()) {
      toBuilder.setUpdateTimeOffsetDateTime(timestampCodec.decode(from.getUpdateTime()));
    }
    if (from.hasCreateTime()) {
      toBuilder.setCreateTimeOffsetDateTime(timestampCodec.decode(from.getCreateTime()));
    }
    if (from.hasCustomTime()) {
      toBuilder.setCustomTimeOffsetDateTime(timestampCodec.decode(from.getCustomTime()));
    }
    if (from.hasCustomerEncryption()) {
      toBuilder.setCustomerEncryption(customerEncryptionCodec.decode(from.getCustomerEncryption()));
    }
    if (from.hasSoftDeleteTime()) {
      toBuilder.setSoftDeleteTime(timestampCodec.decode(from.getSoftDeleteTime()));
    }
    if (from.hasHardDeleteTime()) {
      toBuilder.setHardDeleteTime(timestampCodec.decode(from.getHardDeleteTime()));
    }
    String storageClass = from.getStorageClass();
    if (!storageClass.isEmpty()) {
      toBuilder.setStorageClass(StorageClass.valueOf(storageClass));
    }
    if (from.hasUpdateStorageClassTime()) {
      toBuilder.setTimeStorageClassUpdatedOffsetDateTime(
          timestampCodec.decode(from.getUpdateStorageClassTime()));
    }
    if (!from.getKmsKey().isEmpty()) {
      toBuilder.setKmsKeyName(from.getKmsKey());
    }
    if (from.hasEventBasedHold()) {
      toBuilder.setEventBasedHold(from.getEventBasedHold());
    }
    toBuilder.setTemporaryHold(from.getTemporaryHold());
    if (from.hasRetentionExpireTime()) {
      toBuilder.setRetentionExpirationTimeOffsetDateTime(
          timestampCodec.decode(from.getRetentionExpireTime()));
    }
    if (!from.getMetadataMap().isEmpty()) {
      toBuilder.setMetadata(from.getMetadataMap());
    }
    if (from.hasOwner()) {
      Owner owner = from.getOwner();
      if (!owner.getEntity().isEmpty()) {
        toBuilder.setOwner(entityDecode(owner.getEntity()));
      }
    }
    if (!from.getEtag().isEmpty()) {
      toBuilder.setEtag(from.getEtag());
    }
    ifNonNull(from.getAclList(), toImmutableListOf(objectAcl()::decode), toBuilder::setAcl);
    return toBuilder.build();
  }

  private com.google.iam.v1.Policy policyEncode(Policy from) {
    com.google.iam.v1.Policy.Builder to = com.google.iam.v1.Policy.newBuilder();
    ifNonNull(from.getEtag(), byteStringB64StringCodec::decode, to::setEtag);
    ifNonNull(from.getVersion(), to::setVersion);
    from.getBindingsList().stream().map(bindingCodec::encode).forEach(to::addBindings);
    return to.build();
  }

  private Policy policyDecode(com.google.iam.v1.Policy from) {
    Policy.Builder to = Policy.newBuilder();
    ByteString etag = from.getEtag();
    if (!etag.isEmpty()) {
      to.setEtag(byteStringB64StringCodec.encode(etag));
    }
    to.setVersion(from.getVersion());
    List bindingsList = from.getBindingsList();
    if (!bindingsList.isEmpty()) {
      ImmutableList bindings =
          bindingsList.stream().map(bindingCodec::decode).collect(ImmutableList.toImmutableList());
      to.setBindings(bindings);
    }
    return to.build();
  }

  private com.google.iam.v1.Binding bindingEncode(Binding from) {
    com.google.iam.v1.Binding.Builder to = com.google.iam.v1.Binding.newBuilder();
    ifNonNull(from.getRole(), to::setRole);
    ImmutableList members = from.getMembers();
    if (!members.isEmpty()) {
      to.addAllMembers(members);
    }
    ifNonNull(from.getCondition(), iamConditionCodec::encode, to::setCondition);
    return to.build();
  }

  private Binding bindingDecode(com.google.iam.v1.Binding from) {
    Binding.Builder to = Binding.newBuilder();
    String role = from.getRole();
    if (!role.isEmpty()) {
      to.setRole(role);
    }
    ProtocolStringList membersList = from.getMembersList();
    if (!membersList.isEmpty()) {
      to.setMembers(membersList);
    }
    if (from.hasCondition()) {
      to.setCondition(iamConditionCodec.decode(from.getCondition()));
    }
    return to.build();
  }

  private Expr conditionEncode(Condition from) {
    Expr.Builder to = Expr.newBuilder();
    ifNonNull(from.getExpression(), to::setExpression);
    ifNonNull(from.getTitle(), to::setTitle);
    ifNonNull(from.getDescription(), to::setDescription);
    // apiary doesn't have a "location" field like grpc does
    return to.build();
  }

  private Condition conditionDecode(Expr from) {
    Condition.Builder to = Condition.newBuilder();
    String expression = from.getExpression();
    if (!expression.isEmpty()) {
      to.setExpression(expression);
    }
    String title = from.getTitle();
    if (!title.isEmpty()) {
      to.setTitle(title);
    }
    String description = from.getDescription();
    if (!description.isEmpty()) {
      to.setDescription(description);
    }
    return to.build();
  }

  private Map removeNullValues(Map from) {
    Map to = new HashMap<>(from);
    to.values().removeAll(Collections.singleton(null));
    return to;
  }

  private String removeKmsVersion(String from) {
    PathTemplate versionedKmsTemplate =
        PathTemplate.createWithoutUrlEncoding(
            "projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}/cryptoKeyVersions/{version}");
    if (versionedKmsTemplate.matches(from)) {
      Map res = versionedKmsTemplate.match(from);
      return CryptoKeyName.format(
          res.get("project"), res.get("location"), res.get("key_ring"), res.get("crypto_key"));
    }
    return from;
  }

  private static  T todo() {
    throw new IllegalStateException("Not yet implemented");
  }

  /**
   * Several properties are translating lists of one type to another. This convenience method allows
   * specifying a mapping function and composing as part of an {@code #isNonNull} definition.
   */
  private static  Function, ImmutableList> toImmutableListOf(
      Function f) {
    return l -> l.stream().map(f).collect(ImmutableList.toImmutableList());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy