content: /*
   *  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.causeway.core.metamodel.services.classsubstitutor;
  
  import java.util.Objects;
  import java.util.Set;
  
  import org.apache.causeway.commons.internal.collections._Sets;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.commons.internal.proxy.ProxyFactoryService;
  import org.apache.causeway.commons.internal.reflection._ClassCache;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
  import org.apache.causeway.core.metamodel.commons.ClassUtil;
  
  import org.jspecify.annotations.NonNull;
  import lombok.extern.log4j.Log4j2;
  
  @Log4j2
  public abstract class ClassSubstitutorAbstract implements ClassSubstitutor {
  
      private final _ClassCache classCache = _ClassCache.getInstance();
  
      @Override
      public final Substitution getSubstitution(final @NonNull Class<?> cls) {
          var replacement = getReplacement(cls);
          if(Objects.equals(cls, replacement)) {
              return Substitution.passThrough(); // indifferent
          }
          if(replacement==null) {
              return Substitution.neverIntrospect();
          }
          return Substitution.replaceWith(replacement) ;
      }
  
      protected Class<?> getReplacement(final Class<?> cls) {
  
          if(cls == null) {
              return null;
          }
  
          if(proxyPackageNamesToSkip.stream()
                  .anyMatch(packageName -> cls.getName().startsWith(packageName))) {
              return getReplacement(cls.getSuperclass());
          }
  
          if (shouldIgnore(cls)) {
              return null;
          }
  
          // primarily to ignore unit test fixtures if they happen to be on the classpath.
          // (we can't simply ignore them; for example ApplicationFeatureType enum
          // uses anonymous inner classes and these *are* part of the metamodel)
          if(cls.isAnonymousClass()) {
              return cls.getSuperclass();
          }
  
          final Class<?> superclass = cls.getSuperclass();
          if(superclass != null && superclass.isEnum()) {
              return superclass;
          }
          if (ClassUtil.directlyImplements(cls, ProxyFactoryService.ProxyEnhanced.class)) {
              // REVIEW: arguably this should now go back to the ClassSubstitorRegistry
              return getReplacement(cls.getSuperclass());
          }
  
          try {
              // guard against cannot introspect
              classCache.add(cls);
          } catch (Throwable e) {
  
              log.warn("cannot introspect type {}, adding it to list of ignored types;\n reason: {}",
                      cls.getName(),
                      _Exceptions.getMessage(e));
  
              classesToIgnore.add(cls);
              return null;
          }
  
          return cls;
      }
  
      // -- HELPERS
  
      private final Set<Class<?>> classesToIgnore = _Sets.newConcurrentHashSet();
      private final Set<String> classNamesToIgnore = _Sets.newHashSet();
      private final Set<String> packageNamesToIgnore = _Sets.newHashSet();
      private final Set<String> proxyPackageNamesToSkip = _Sets.newHashSet();
  
      /**
       * For any classes registered as ignored, {@link #getReplacement(Class)} will
       * return <tt>null</tt>.
       */
      protected void ignoreClass(final String className) {
          classNamesToIgnore.add(className);
      }
  
      protected void ignorePackage(final String packageName) {
          packageNamesToIgnore.add(packageName);
      }
  
      /**
       * Any classes in these package represent a proxy (eg DN-enhanced bytecode).
       * We skip the proxy class itself but traverse up to the supertype.
       */
      protected void skipProxyPackage(final String packageName) {
          proxyPackageNamesToSkip.add(packageName);
      }
  
      private boolean shouldIgnore(final Class<?> cls) {
          if (cls.isArray()) {
              return shouldIgnore(cls.getComponentType());
          }
  
          // ignore vetoed types
          if(ProgrammingModelConstants.TypeVetoMarker.anyMatchOn(cls)) {
              return true;
          }
  
          var className = cls.getName();
  
          try{
              return classesToIgnore.contains(cls)
                      || classNamesToIgnore.contains(cls.getCanonicalName())
                      || packageNamesToIgnore.stream().anyMatch(className::startsWith);
  
          } catch(NoClassDefFoundError e) {
  
              try{
                  if(cls.isAnonymousClass()) {
                      return shouldIgnore(cls.getSuperclass());
                  } else {
                      return false;
                  }
              } catch(NoClassDefFoundError ex) {
                  return true;
              }
          }
      }
  
  }
  