private TemplateModel getInternal(String key)
      throws TemplateModelException, ClassNotFoundException {
    {
      TemplateModel model = (TemplateModel) cache.get(key);
      if (model != null) return model;
    }

    final ClassIntrospector classIntrospector;
    int classIntrospectorClearingCounter;
    final Object sharedLock = wrapper.getSharedIntrospectionLock();
    synchronized (sharedLock) {
      TemplateModel model = (TemplateModel) cache.get(key);
      if (model != null) return model;

      while (model == null && classIntrospectionsInProgress.contains(key)) {
        // Another thread is already introspecting this class;
        // waiting for its result.
        try {
          sharedLock.wait();
          model = (TemplateModel) cache.get(key);
        } catch (InterruptedException e) {
          throw new RuntimeException("Class inrospection data lookup aborded: " + e);
        }
      }
      if (model != null) return model;

      // This will be the thread that introspects this class.
      classIntrospectionsInProgress.add(key);

      // While the classIntrospector should not be changed from another thread, badly written apps
      // can do that,
      // and it's cheap to get the classIntrospector from inside the lock here:
      classIntrospector = wrapper.getClassIntrospector();
      classIntrospectorClearingCounter = classIntrospector.getClearingCounter();
    }
    try {
      final Class clazz = ClassUtil.forName(key);

      // This is called so that we trigger the
      // class-reloading detector. If clazz is a reloaded class,
      // the wrapper will in turn call our clearCache method.
      // TODO: Why do we check it now and only now?
      classIntrospector.get(clazz);

      TemplateModel model = createModel(clazz);
      // Warning: model will be null if the class is not good for the subclass.
      // For example, EnumModels#createModel returns null if clazz is not an enum.

      if (model != null) {
        synchronized (sharedLock) {
          // Save it into the cache, but only if nothing relevant has changed while we were outside
          // the lock:
          if (classIntrospector == wrapper.getClassIntrospector()
              && classIntrospectorClearingCounter == classIntrospector.getClearingCounter()) {
            cache.put(key, model);
          }
        }
      }
      return model;
    } finally {
      synchronized (sharedLock) {
        classIntrospectionsInProgress.remove(key);
        sharedLock.notifyAll();
      }
    }
  }