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

com.google.appengine.tools.compilation.DatastoreCallbacksConfigWriter Maven / Gradle / Ivy

/*
 * Copyright 2021 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
 *
 *     https://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.appengine.tools.compilation;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Helper that keeps track of the callbacks we encounter and writes them out in
 * the appropriate format.
 *
 * @see DatastoreCallbacksProcessor
 *
 */
class DatastoreCallbacksConfigWriter {

  static final String INCORRECT_FORMAT_MESSAGE = "Existing config file has incorrect "
      + "format version. Please do a clean rebuild of your application.";

  /**
   * Property that stores the format version of the individual entries.  This
   * property is not a valid callback key, so there is no risk of a collision with
   * real data.
   */
  static final String FORMAT_VERSION_PROPERTY = "DatastoreCallbacksFormatVersion";

  /**
   * Right now we only have one format version, so we can hardcode this.
   */
  private static final String FORMAT_VERSION = Integer.toString(1);

  /**
   * Key is the kind (possibly the empty string), value is a {@link Map} where
   * the key is the callback type and the value is a {@link List} of
   * {@link String Strings}, each of which uniquely identifies a method that
   * implements the callback for the kind and callback type.  Each of these
   * Strings is of the form fqn:method_name.  We're not using a LinkedHashMap
   * to make the iteration order deterministic because we're using
   * {@link Properties} to write this data to a file, and Properties, unlike
   * LinkedHashMap, does not have a deterministic ordering.
   */
  /* @VisibleForTesting */
  final Map> callbacks = Maps.newHashMap();

  /**
   * Maps the fully-qualified classnames of all classes with callbacks to the methods on those
   * classes that are callbacks.
   */
  /* @VisibleForTesting */
  final SetMultimap methodsWithCallbacks = LinkedHashMultimap.create();

  // We'll just use Properties to write the data.  There's no way to ensure
  // that the data gets written out in the same order every time, which makes
  // testing a little bit harder, but it's also nice to let this class handle
  // reading and writing the file.
  final Properties props = new Properties();

  /**
   * Used to avoid logging about the same pruned class more than once.  This
   * member should not be read until after {@link #store(java.io.OutputStream)}
   * has been called.
   */
  final Set prunedClasses = Sets.newHashSet();

  /**
   * @param inputStream a (possibly {@code null} stream from which we can read an existing config
   */
  DatastoreCallbacksConfigWriter(@Nullable InputStream inputStream) throws IOException {
    if (inputStream != null) {
      props.loadFromXML(inputStream);
      // We'll add the version back when we store the config
      Preconditions.checkState(
          FORMAT_VERSION.equals(props.remove(FORMAT_VERSION_PROPERTY)), INCORRECT_FORMAT_MESSAGE);
    }
  }

  /**
   * Overwrites the contents of the provided {@link InputStream} (if provided)
   * with the callback config and then stores it to disk.
   */
  public void store(OutputStream outputStream) throws IOException {
    pruneExistingConfig();

    // Add a version marker so that it's easy to change the format in the future.
    props.setProperty(FORMAT_VERSION_PROPERTY, FORMAT_VERSION);

    for (String kind : callbacks.keySet()) {
      Multimap kindMap = callbacks.get(kind);
      for (String callbackType : kindMap.keySet()) {
        String propKey = String.format("%s.%s", kind, callbackType);
        // We've already purged everything that should no longer exist, so now
        // merge old and new methods for this key.
        Collection newMethods = kindMap.get(callbackType);
        StringBuilder combinedMethods = new StringBuilder();
        String oldMethods = props.getProperty(propKey);
        if (oldMethods != null) {
          combinedMethods.append(oldMethods).append(",");
        }
        props.setProperty(propKey, Joiner.on(",").appendTo(combinedMethods, newMethods).toString());
      }
    }
    // Write the data as XML.
    props.storeToXML(outputStream, "Datastore Callbacks. DO NOT EDIT BY HAND!");
  }

  private void pruneExistingConfig() {
    // props may contain existing data.  Since the annotation processor doesn't
    // get invoked when a class that used to have annotations is deleted, or
    // when a method that used to have annotations is compiled, we need to
    // remove references to these classes and methods ourselves.  We'll
    // remove all references to classes that have been run through the
    // annotation processor.  The most up-to-date config for these classes will be
    // contained in callbacks so we're guaranteed to end up with the correct
    // config.  We'll also remove all references to classes that cannot be
    // loaded because if the class is not on the classpath it either no longer
    // exists (in which case we want to remove the references) or it is going
    // to get compiled (in which case the correct references will get
    // regenerated anyway).
    for (String propName : props.stringPropertyNames()) {
      String propVal = props.getProperty(propName);
      List methodsToPreserve = Lists.newArrayList();
      for (String method : Splitter.on(',').split(propVal)) {
        String classStr = Iterables.get(Splitter.on(':').split(method), 0);
        if (!methodsWithCallbacks.containsKey(classStr) && classExists(classStr)) {
          // the class still exists and is not one that we've processed so
          // leave the reference to the method
          methodsToPreserve.add(method);
        }
      }
      if (methodsToPreserve.isEmpty()) {
        props.remove(propName);
      } else {
        props.setProperty(propName, Joiner.on(",").join(methodsToPreserve));
      }
    }
  }

  private boolean classExists(String classStr) {
    try {
      Class.forName(classStr);
      return true;
    } catch (ClassNotFoundException e) {
      prunedClasses.add(classStr);
      return false;
    }
  }

  public void addCallback(Set kinds, String callbackType, String cls, String method) {
    String clsMethod = String.format("%s:%s", cls, method);
    if (kinds.isEmpty()) {
      // wildcard callback that matches all kinds.
      kinds = ImmutableSet.of("");
    }
    for (String kind : kinds) {
      Multimap kindMap =
          callbacks.computeIfAbsent(kind, (String k) -> LinkedHashMultimap.create());
      kindMap.put(callbackType, clsMethod);
    }
    methodsWithCallbacks.put(cls, method);
  }

  @Override
  public String toString() {
    return String.format("Datastore Callbacks: %s\nPruned Classes: %s", callbacks, prunedClasses);
  }

  public boolean hasCallback(String cls, String method) {
    return methodsWithCallbacks.containsEntry(cls, method);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy