public AssetSourceImpl(
      ThreadLocale threadLocale,
      Map<String, AssetFactory> configuration,
      SymbolSource symbolSource,
      Logger logger,
      OperationTracker tracker,
      Request request) {
    this.configuration = configuration;
    this.threadLocale = threadLocale;
    this.symbolSource = symbolSource;
    this.logger = logger;
    this.tracker = tracker;
    this.request = request;

    Map<Class, AssetFactory> byResourceClass = CollectionFactory.newMap();

    for (Map.Entry<String, AssetFactory> e : configuration.entrySet()) {
      String prefix = e.getKey();
      AssetFactory factory = e.getValue();

      Resource rootResource = factory.getRootResource();

      byResourceClass.put(rootResource.getClass(), factory);

      prefixToRootResource.put(prefix, rootResource);
    }

    registry = StrategyRegistry.newInstance(AssetFactory.class, byResourceClass);
  }
  private Map<String, Object> getASOMap() {
    Map<String, Object> result = (Map<String, Object>) request.getAttribute(ASO_MAP_ATTRIBUTE);

    if (result == null) {
      result = CollectionFactory.newMap();
      request.setAttribute(ASO_MAP_ATTRIBUTE, result);
    }

    return result;
  }
  public Map<String, Binding> getInformalParameterBindings() {
    Map<String, Binding> result = CollectionFactory.newMap();

    for (String name : NamedSet.getNames(bindings)) {
      if (componentModel.getParameterModel(name) != null) continue;

      result.put(name, bindings.get(name));
    }

    return result;
  }
Beispiel #4
0
 @Test
 public void submit_form() {
   String appPackage = "org.apache.tapestry5.integration.app2";
   String appName = "";
   tester = new PageTester(appPackage, appName);
   Document doc = tester.renderPage("TestPageForForm");
   Element form = doc.getElementById("form1");
   Map<String, String> fieldValues = CollectionFactory.newMap();
   fieldValues.put("t1", "hello");
   doc = tester.submitForm(form, fieldValues);
   assertTrue(doc.toString().contains("You entered: hello"));
 }
  /**
   * Returns a <em>new</em> selector with the given axis data. It is not allowed to redefine an
   * existing axis type. Typically, the axis type is an enum type. Axis values are expected to be
   * immutable, and to implement {@code equals()} and {@code hashCode()}.
   *
   * @param axisType non-blank axis key
   * @param axisValue non-null axis value
   * @return new selector including axis value
   */
  public <T> ComponentResourceSelector withAxis(Class<T> axisType, T axisValue) {
    assert axisType != null;
    assert axisValue != null;

    if (axis.containsKey(axisType))
      throw new IllegalArgumentException(
          String.format(
              "Axis type %s is already specified as %s.", axisType.getName(), axis.get(axisType)));

    Map<Class, Object> updated = CollectionFactory.newMap(axis);
    updated.put(axisType, axisValue);

    return new ComponentResourceSelector(locale, updated);
  }
  @Override
  public Object createInterceptor(Object delegate) {
    // Create a copy of the parameters map so that Object.class points to the delegate instance.

    Map<Class, Object> resources = CollectionFactory.newMap(this.resourcesDefaults);

    resources.put(Object.class, delegate);
    resources.put(serviceInterface, delegate);

    InjectionResources injectionResources = new MapInjectionResources(resources);

    Object result = invoke(injectionResources);

    if (result != null && !serviceInterface.isInstance(result)) {
      throw new RuntimeException(
          IOCMessages.decoratorReturnedWrongType(method, serviceId, result, serviceInterface));
    }

    return result;
  }
  private void startComponent(StartComponentToken token) {
    String elementName = token.getElementName();

    // Initial guess: the type from the token (but this may be null in many cases).
    String embeddedType = token.getComponentType();

    // This may be null for an anonymous component.
    String embeddedId = token.getId();

    String embeddedComponentClassName = null;

    final EmbeddedComponentModel embeddedModel =
        embeddedId == null ? null : loadingComponentModel.getEmbeddedComponentModel(embeddedId);

    // We know that if embeddedId is null, embeddedType is not.

    if (embeddedId == null) embeddedId = generateEmbeddedId(embeddedType, idAllocator);

    if (embeddedModel != null) {
      String modelType = embeddedModel.getComponentType();

      if (InternalUtils.isNonBlank(modelType) && embeddedType != null)
        throw new TapestryException(
            ServicesMessages.compTypeConflict(embeddedId, embeddedType, modelType), token, null);

      embeddedType = modelType;
      embeddedComponentClassName = embeddedModel.getComponentClassName();
    }

    // We only have the embeddedModel if the embeddedId was specified.  If embeddedType was ommitted
    // and

    if (InternalUtils.isBlank(embeddedType) && embeddedModel == null)
      throw new TapestryException(
          ServicesMessages.noTypeForEmbeddedComponent(
              embeddedId, loadingComponentModel.getComponentClassName()),
          token,
          null);

    final ComponentPageElement newComponent =
        pageElementFactory.newComponentElement(
            page,
            loadingElement,
            embeddedId,
            embeddedType,
            embeddedComponentClassName,
            elementName,
            token.getLocation());

    addMixinsToComponent(newComponent, embeddedModel, token.getMixins());

    final Map<String, Binding> newComponentBindings = CollectionFactory.newMap();
    componentIdToBindingMap.put(newComponent.getCompleteId(), newComponentBindings);

    if (embeddedModel != null)
      bindParametersFromModel(embeddedModel, loadingElement, newComponent, newComponentBindings);

    addToBody(newComponent);

    // Remember to load the template for this new component
    componentQueue.push(newComponent);

    // Any attribute tokens that immediately follow should be
    // used to bind parameters.

    addAttributesAsComponentBindings = true;

    // Any attributes (including component parameters) that come up belong on this component.

    activeElementStack.push(newComponent);

    // Set things up so that content inside the component is added to the component's body.

    bodyPageElementStack.push(newComponent);

    // And clean that up when the end element is reached.

    final ComponentModel newComponentModel =
        newComponent.getComponentResources().getComponentModel();

    // If the component was from an embedded @Component annotation, and it is inheritting informal
    // parameters,
    // and the component in question supports informal parameters, than get those inheritted
    // informal parameters ...
    // but later (this helps ensure that <t:parameter> elements that may provide informal parameters
    // are
    // visible when the informal parameters are copied to the child component).

    if (embeddedModel != null
        && embeddedModel.getInheritInformalParameters()
        && newComponentModel.getSupportsInformalParameters()) {
      final ComponentPageElement loadingElement = this.loadingElement;

      Runnable finalizer =
          new Runnable() {
            public void run() {
              handleInformalParameters(
                  loadingElement,
                  embeddedModel,
                  newComponent,
                  newComponentModel,
                  newComponentBindings);
            }
          };

      finalization.add(finalizer);
    }

    Runnable cleanup =
        new Runnable() {
          public void run() {
            // May need a separate queue for this, to execute at the very end of page loading.

            activeElementStack.pop();
            bodyPageElementStack.pop();
          }
        };

    // The start tag is not added to the body of the component, so neither should
    // the end tag.
    configureEnd(true, cleanup);
  }
/**
 * Contains all the work-state related to the {@link PageLoaderImpl}.
 *
 * <p><em>This is the Tapestry heart, this is the Tapestry soul ...</em>
 */
class PageLoaderProcessor {
  /** Special prefix for parameters that are inherited from named parameters of their container. */
  private static final String INHERIT_PREFIX = "inherit:";

  private static final Runnable NO_OP =
      new Runnable() {
        public void run() {
          // Do nothing.
        }
      };

  private static final Pattern COMMA_PATTERN = Pattern.compile(",");

  private Stack<ComponentPageElement> activeElementStack = CollectionFactory.newStack();

  private boolean addAttributesAsComponentBindings = false;

  private boolean dtdAdded;

  private final Stack<BodyPageElement> bodyPageElementStack = CollectionFactory.newStack();

  // You can use a stack as a queue
  private final Stack<ComponentPageElement> componentQueue = CollectionFactory.newStack();

  private final Stack<Boolean> discardEndTagStack = CollectionFactory.newStack();

  private final Stack<Runnable> endElementCommandStack = CollectionFactory.newStack();

  /** Used as a queue of Runnable objects used to handle final setup. */
  private final List<Runnable> finalization = CollectionFactory.newList();

  private final IdAllocator idAllocator = new IdAllocator();

  private final LinkFactory linkFactory;

  private final ComponentClassResolver componentClassResolver;

  private ComponentModel loadingComponentModel;

  private ComponentPageElement loadingElement;

  private final Map<String, Map<String, Binding>> componentIdToBindingMap =
      CollectionFactory.newMap();

  private Locale locale;

  private final OneShotLock lock = new OneShotLock();

  private Page page;

  private final PageElementFactory pageElementFactory;

  private final PersistentFieldManager persistentFieldManager;

  private final ComponentTemplateSource templateSource;

  private static final PageElement END_ELEMENT =
      new PageElement() {
        public void render(MarkupWriter writer, RenderQueue queue) {
          writer.end();
        }

        @Override
        public String toString() {
          return "End";
        }
      };

  private static class RenderBodyElement implements PageElement {
    private final ComponentPageElement component;

    public RenderBodyElement(ComponentPageElement component) {
      this.component = component;
    }

    public void render(MarkupWriter writer, RenderQueue queue) {
      component.enqueueBeforeRenderBody(queue);
    }

    @Override
    public String toString() {
      return String.format("RenderBody[%s]", component.getNestedId());
    }
  }

  PageLoaderProcessor(
      ComponentTemplateSource templateSource,
      PageElementFactory pageElementFactory,
      LinkFactory linkFactory,
      PersistentFieldManager persistentFieldManager,
      ComponentClassResolver componentClassResolver) {
    this.templateSource = templateSource;
    this.pageElementFactory = pageElementFactory;
    this.linkFactory = linkFactory;
    this.persistentFieldManager = persistentFieldManager;
    this.componentClassResolver = componentClassResolver;
  }

  private void bindParameterFromTemplate(ComponentPageElement component, AttributeToken token) {
    String name = token.getName();
    ComponentResources resources = component.getComponentResources();

    // If already bound (i.e., from the component class, via @Component), then
    // ignore the value in the template. This may need improving to just ignore
    // the value if it is an unprefixed literal string.

    if (resources.isBound(name)) return;

    // Meta default of literal for the template.

    String defaultBindingPrefix =
        determineDefaultBindingPrefix(component, name, BindingConstants.LITERAL);

    Binding binding =
        findBinding(
            loadingElement,
            component,
            name,
            token.getValue(),
            defaultBindingPrefix,
            token.getLocation());

    if (binding != null) {
      component.bindParameter(name, binding);

      Map<String, Binding> bindingMap = componentIdToBindingMap.get(component.getCompleteId());
      bindingMap.put(name, binding);
    }
  }

  private void addMixinsToComponent(
      ComponentPageElement component, EmbeddedComponentModel model, String mixins) {
    if (model != null) {
      for (String mixinClassName : model.getMixinClassNames())
        pageElementFactory.addMixinByClassName(component, mixinClassName);
    }

    if (mixins != null) {
      for (String type : COMMA_PATTERN.split(mixins))
        pageElementFactory.addMixinByTypeName(component, type);
    }
  }

  /**
   * @param model embededded model defining the new component, from an {@link
   *     org.apache.tapestry5.annotations.Component} annotation
   * @param loadingComponent the currently loading container component
   * @param newComponent the new child of the container whose parameters are being bound
   * @param newComponentBindings map of bindings for the new component (used to handle inheriting of
   *     informal parameters)
   */
  private void bindParametersFromModel(
      EmbeddedComponentModel model,
      ComponentPageElement loadingComponent,
      ComponentPageElement newComponent,
      Map<String, Binding> newComponentBindings) {
    for (String name : model.getParameterNames()) {
      String value = model.getParameterValue(name);

      String defaultBindingPrefix =
          determineDefaultBindingPrefix(newComponent, name, BindingConstants.PROP);

      Binding binding =
          findBinding(
              loadingComponent,
              newComponent,
              name,
              value,
              defaultBindingPrefix,
              newComponent.getLocation());

      if (binding != null) {
        newComponent.bindParameter(name, binding);

        // So that the binding can be shared if inherited by a subcomponent
        newComponentBindings.put(name, binding);
      }
    }
  }

  /**
   * Creates a new binding, or returns an existing binding (or null) for the "inherit:" binding prefix. Mostly a
   * wrapper around {@link BindingSource#newBinding(String, ComponentResources, ComponentResources, String, String,
   * Location)
   *
   * @return the new binding, or an existing binding (if inherited), or null (if inherited, and the containing
   *         parameter is not bound)
   */
  private Binding findBinding(
      ComponentPageElement loadingComponent,
      ComponentPageElement component,
      String name,
      String value,
      String defaultBindingPrefix,
      Location location) {
    if (value.startsWith(INHERIT_PREFIX)) {
      String loadingParameterName = value.substring(INHERIT_PREFIX.length());
      Map<String, Binding> loadingComponentBindingMap =
          componentIdToBindingMap.get(loadingComponent.getCompleteId());

      // This may return null if the parameter is not bound in the loading component.

      Binding existing = loadingComponentBindingMap.get(loadingParameterName);

      if (existing == null) return null;

      String description =
          String.format(
              "InheritedBinding[parameter %s %s(inherited from %s of %s)]",
              name,
              component.getCompleteId(),
              loadingParameterName,
              loadingComponent.getCompleteId());

      // This helps with debugging, and re-orients any thrown exceptions
      // to the location of the inherited binding, rather than the container component's
      // binding.

      return new InheritedBinding(description, existing, location);
    }

    return pageElementFactory.newBinding(
        name,
        loadingComponent.getComponentResources(),
        component.getComponentResources(),
        defaultBindingPrefix,
        value,
        location);
  }

  /**
   * Determines the default binding prefix for a particular parameter.
   *
   * @param component the component which will have a parameter bound
   * @param parameterName the name of the parameter
   * @param informalParameterBindingPrefix the default to use for informal parameters
   * @return the binding prefix
   */
  private String determineDefaultBindingPrefix(
      ComponentPageElement component, String parameterName, String informalParameterBindingPrefix) {
    String defaultBindingPrefix = component.getDefaultBindingPrefix(parameterName);

    return defaultBindingPrefix != null ? defaultBindingPrefix : informalParameterBindingPrefix;
  }

  private void addToBody(PageElement element) {
    bodyPageElementStack.peek().addToBody(element);
  }

  private void attribute(AttributeToken token) {
    // This kind of bookkeeping is ugly, we probably should have distinct (if very similar)
    // tokens for attributes and for parameter bindings.

    if (addAttributesAsComponentBindings) {
      ComponentPageElement activeElement = activeElementStack.peek();

      bindParameterFromTemplate(activeElement, token);
      return;
    }

    PageElement element =
        pageElementFactory.newAttributeElement(loadingElement.getComponentResources(), token);

    addToBody(element);
  }

  private void body() {
    addToBody(new RenderBodyElement(loadingElement));

    // BODY tokens are *not* matched by END_ELEMENT tokens. Nor will there be
    // text or comment content "inside" the BODY.
  }

  private void comment(CommentToken token) {
    PageElement commentElement = new CommentPageElement(token.getComment());

    addToBody(commentElement);
  }

  /**
   * Invoked whenever a token (start, startComponent, etc.) is encountered that will eventually have
   * a matching end token. Sets up the behavior for the end token.
   *
   * @param discard if true, the end is discarded (if false the end token is added to the active
   *     body element)
   * @param command command to execute to return processor state back to what it was before the
   *     command executed
   */
  private void configureEnd(boolean discard, Runnable command) {
    discardEndTagStack.push(discard);
    endElementCommandStack.push(command);
  }

  private void endElement() {
    // discard will be false if the matching start token was for a static element, and will be
    // true otherwise (component, block, parameter).

    boolean discard = discardEndTagStack.pop();

    if (!discard) addToBody(END_ELEMENT);

    Runnable command = endElementCommandStack.pop();

    // Used to return environment to prior state.

    command.run();
  }

  private void expansion(ExpansionToken token) {
    PageElement element =
        pageElementFactory.newExpansionElement(loadingElement.getComponentResources(), token);

    addToBody(element);
  }

  private String generateEmbeddedId(String embeddedType, IdAllocator idAllocator) {
    // Component types may be in folders; strip off the folder part for starters.

    int slashx = embeddedType.lastIndexOf("/");

    String baseId = embeddedType.substring(slashx + 1).toLowerCase();

    // The idAllocator is pre-loaded with all the component ids from the template, so even
    // if the lower-case type matches the id of an existing component, there won't be a name
    // collision.

    return idAllocator.allocateId(baseId);
  }

  /**
   * As currently implemented, this should be invoked just once and then the PageLoaderProcessor
   * instance should be discarded.
   */
  public Page loadPage(String logicalPageName, String pageClassName, Locale locale) {
    // Ensure that loadPage() may only be invoked once.

    lock.lock();

    this.locale = locale;

    // Todo: Need a resources object for Pages, not just ComponentPageElement ... too many
    // parameters here.

    page =
        new PageImpl(
            logicalPageName,
            this.locale,
            linkFactory,
            persistentFieldManager,
            componentClassResolver);

    loadRootComponent(pageClassName);

    workComponentQueue();

    // Take care of any finalization logic that's been deferred out.

    for (Runnable r : finalization) {
      r.run();
    }

    // The page is *loaded* before it is attached to the request.
    // This is to help ensure that no client-specific information leaks
    // into the page.

    page.loaded();

    return page;
  }

  private void loadRootComponent(String className) {
    ComponentPageElement rootComponent =
        pageElementFactory.newRootComponentElement(page, className, locale);

    page.setRootElement(rootComponent);

    componentQueue.push(rootComponent);
  }

  /**
   * Do you smell something? I'm smelling that this class needs to be redesigned to not need a
   * central method this large and hard to test. I think a lot of instance and local variables need
   * to be bundled up into some kind of process object. This code is effectively too big to be
   * tested except through integration testing.
   */
  private void loadTemplateForComponent(final ComponentPageElement loadingElement) {
    this.loadingElement = loadingElement;
    loadingComponentModel = loadingElement.getComponentResources().getComponentModel();

    String componentClassName = loadingComponentModel.getComponentClassName();
    ComponentTemplate template = templateSource.getTemplate(loadingComponentModel, locale);

    // When the template for a component is missing, we pretend it consists of just a RenderBody
    // phase. Missing is not an error ... many component simply do not have a template.

    if (template.isMissing()) {
      this.loadingElement.addToTemplate(new RenderBodyElement(this.loadingElement));
      return;
    }

    // Pre-allocate ids to avoid later name collisions.

    // Don't have a case-insensitive Set, so we'll make due with a Map
    Map<String, Boolean> embeddedIds = collectedEmbeddedComponentIds(loadingComponentModel);

    idAllocator.clear();

    final Map<String, Location> componentIdsMap = template.getComponentIds();

    for (String id : componentIdsMap.keySet()) {
      idAllocator.allocateId(id);
      embeddedIds.remove(id);
    }

    if (!embeddedIds.isEmpty())
      throw new RuntimeException(
          ServicesMessages.embeddedComponentsNotInTemplate(
              embeddedIds.keySet(), componentClassName, template.getResource()));

    addAttributesAsComponentBindings = false;

    // The outermost elements of the template belong in the loading component's template list,
    // not its body list. This shunt allows everyone else to not have to make that decision,
    // they can add to the "body" and (if there isn't an active component), the shunt will
    // add the element to the component's template.

    BodyPageElement shunt =
        new BodyPageElement() {
          public void addToBody(PageElement element) {
            loadingElement.addToTemplate(element);
          }
        };

    bodyPageElementStack.push(shunt);

    for (TemplateToken token : template.getTokens()) {
      switch (token.getTokenType()) {
        case TEXT:
          text((TextToken) token);
          break;

        case EXPANSION:
          expansion((ExpansionToken) token);
          break;

        case BODY:
          body();
          break;

        case START_ELEMENT:
          startElement((StartElementToken) token);
          break;

        case START_COMPONENT:
          startComponent((StartComponentToken) token);
          break;

        case ATTRIBUTE:
          attribute((AttributeToken) token);
          break;

        case END_ELEMENT:
          endElement();
          break;

        case COMMENT:
          comment((CommentToken) token);
          break;

        case BLOCK:
          block((BlockToken) token);
          break;

        case PARAMETER:
          parameter((ParameterToken) token);
          break;

        case DTD:
          dtd((DTDToken) token);
          break;

        case DEFINE_NAMESPACE_PREFIX:
          defineNamespacePrefix((DefineNamespacePrefixToken) token);
          break;

        case CDATA:
          cdata((CDATAToken) token);
          break;

        default:
          throw new IllegalStateException("Not implemented yet: " + token);
      }
    }

    // For neatness / symmetry:

    bodyPageElementStack.pop(); // the shunt

    // TODO: Check that all stacks are empty. That should never happen, as long
    // as the ComponentTemplate is valid.
  }

  /**
   * Returns a pseudo-set (a case insensitive map where the values are always true) for all the
   * embedded component ids in the component.
   *
   * @param componentModel
   * @return map whose keys are the embedded component ids
   */
  private Map<String, Boolean> collectedEmbeddedComponentIds(ComponentModel componentModel) {
    Map<String, Boolean> result = CollectionFactory.newCaseInsensitiveMap();

    for (String id : componentModel.getEmbeddedComponentIds()) result.put(id, true);

    return result;
  }

  private void cdata(CDATAToken token) {
    final String content = token.getContent();

    PageElement element =
        new PageElement() {
          public void render(MarkupWriter writer, RenderQueue queue) {
            writer.cdata(content);
          }
        };

    addToBody(element);
  }

  private void defineNamespacePrefix(final DefineNamespacePrefixToken token) {
    PageElement element =
        new PageElement() {
          public void render(MarkupWriter writer, RenderQueue queue) {
            writer.defineNamespace(token.getNamespaceURI(), token.getNamespacePrefix());
          }
        };

    addToBody(element);
  }

  private void parameter(ParameterToken token) {
    ComponentPageElement element = activeElementStack.peek();
    String name = token.getName();

    BlockImpl block =
        new BlockImpl(
            token.getLocation(), String.format("Parmeter %s of %s", name, element.getCompleteId()));

    Binding binding = new LiteralBinding("block parameter " + name, block, token.getLocation());

    // TODO: Check that the t:parameter doesn't appear outside of an embedded component.

    element.bindParameter(name, binding);

    setupBlock(block);
  }

  private void setupBlock(BodyPageElement block) {
    bodyPageElementStack.push(block);

    Runnable cleanup =
        new Runnable() {
          public void run() {
            bodyPageElementStack.pop();
          }
        };

    configureEnd(true, cleanup);
  }

  private void block(BlockToken token) {

    String id = token.getId();
    // Don't use the page element factory here becauses we need something that is both Block and
    // BodyPageElement and don't want to use casts.

    String description =
        id == null
            ? String.format("Anonymous within %s", loadingElement.getCompleteId())
            : String.format("%s within %s", id, loadingElement.getCompleteId());

    BlockImpl block = new BlockImpl(token.getLocation(), description);

    if (id != null) loadingElement.addBlock(id, block);

    setupBlock(block);
  }

  private void startComponent(StartComponentToken token) {
    String elementName = token.getElementName();

    // Initial guess: the type from the token (but this may be null in many cases).
    String embeddedType = token.getComponentType();

    // This may be null for an anonymous component.
    String embeddedId = token.getId();

    String embeddedComponentClassName = null;

    final EmbeddedComponentModel embeddedModel =
        embeddedId == null ? null : loadingComponentModel.getEmbeddedComponentModel(embeddedId);

    // We know that if embeddedId is null, embeddedType is not.

    if (embeddedId == null) embeddedId = generateEmbeddedId(embeddedType, idAllocator);

    if (embeddedModel != null) {
      String modelType = embeddedModel.getComponentType();

      if (InternalUtils.isNonBlank(modelType) && embeddedType != null)
        throw new TapestryException(
            ServicesMessages.compTypeConflict(embeddedId, embeddedType, modelType), token, null);

      embeddedType = modelType;
      embeddedComponentClassName = embeddedModel.getComponentClassName();
    }

    // We only have the embeddedModel if the embeddedId was specified.  If embeddedType was ommitted
    // and

    if (InternalUtils.isBlank(embeddedType) && embeddedModel == null)
      throw new TapestryException(
          ServicesMessages.noTypeForEmbeddedComponent(
              embeddedId, loadingComponentModel.getComponentClassName()),
          token,
          null);

    final ComponentPageElement newComponent =
        pageElementFactory.newComponentElement(
            page,
            loadingElement,
            embeddedId,
            embeddedType,
            embeddedComponentClassName,
            elementName,
            token.getLocation());

    addMixinsToComponent(newComponent, embeddedModel, token.getMixins());

    final Map<String, Binding> newComponentBindings = CollectionFactory.newMap();
    componentIdToBindingMap.put(newComponent.getCompleteId(), newComponentBindings);

    if (embeddedModel != null)
      bindParametersFromModel(embeddedModel, loadingElement, newComponent, newComponentBindings);

    addToBody(newComponent);

    // Remember to load the template for this new component
    componentQueue.push(newComponent);

    // Any attribute tokens that immediately follow should be
    // used to bind parameters.

    addAttributesAsComponentBindings = true;

    // Any attributes (including component parameters) that come up belong on this component.

    activeElementStack.push(newComponent);

    // Set things up so that content inside the component is added to the component's body.

    bodyPageElementStack.push(newComponent);

    // And clean that up when the end element is reached.

    final ComponentModel newComponentModel =
        newComponent.getComponentResources().getComponentModel();

    // If the component was from an embedded @Component annotation, and it is inheritting informal
    // parameters,
    // and the component in question supports informal parameters, than get those inheritted
    // informal parameters ...
    // but later (this helps ensure that <t:parameter> elements that may provide informal parameters
    // are
    // visible when the informal parameters are copied to the child component).

    if (embeddedModel != null
        && embeddedModel.getInheritInformalParameters()
        && newComponentModel.getSupportsInformalParameters()) {
      final ComponentPageElement loadingElement = this.loadingElement;

      Runnable finalizer =
          new Runnable() {
            public void run() {
              handleInformalParameters(
                  loadingElement,
                  embeddedModel,
                  newComponent,
                  newComponentModel,
                  newComponentBindings);
            }
          };

      finalization.add(finalizer);
    }

    Runnable cleanup =
        new Runnable() {
          public void run() {
            // May need a separate queue for this, to execute at the very end of page loading.

            activeElementStack.pop();
            bodyPageElementStack.pop();
          }
        };

    // The start tag is not added to the body of the component, so neither should
    // the end tag.
    configureEnd(true, cleanup);
  }

  /**
   * Invoked when a component's end tag is reached, to check and process informal parameters as per
   * the {@link org.apache.tapestry5.model.EmbeddedComponentModel#getInheritInformalParameters()}
   * flag.
   *
   * @param loadingComponent the container component that was loaded
   * @param model
   * @param newComponent
   * @param newComponentBindings
   */
  private void handleInformalParameters(
      ComponentPageElement loadingComponent,
      EmbeddedComponentModel model,
      ComponentPageElement newComponent,
      ComponentModel newComponentModel,
      Map<String, Binding> newComponentBindings) {

    Map<String, Binding> informals = loadingComponent.getInformalParameterBindings();

    for (String name : informals.keySet()) {
      if (newComponentModel.getParameterModel(name) != null) continue;

      Binding binding = informals.get(name);

      newComponent.bindParameter(name, binding);
      newComponentBindings.put(name, binding);
    }
  }

  private void startElement(StartElementToken token) {
    PageElement element = new StartElementPageElement(token.getNamespaceURI(), token.getName());

    addToBody(element);

    // Controls how attributes are interpretted.
    addAttributesAsComponentBindings = false;

    // Index will be matched by end:

    // Do NOT discard the end tag; add it to the body.

    configureEnd(false, NO_OP);
  }

  private void text(TextToken token) {
    PageElement element = new TextPageElement(token.getText());

    addToBody(element);
  }

  private void dtd(DTDToken token) {
    // first DTD encountered wins.
    if (dtdAdded) return;

    PageElement element =
        new DTDPageElement(token.getName(), token.getPublicId(), token.getSystemId());
    // since rendering via the markup writer is to the document tree,
    // we don't really care where this gets placed in the tree; the
    // DTDPageElement will set the dtd of the document directly, rather than
    // writing anything to the markup writer
    page.getRootElement().addToTemplate(element);

    dtdAdded = true;
  }

  /** Works the component queue, until exausted. */
  private void workComponentQueue() {
    while (!componentQueue.isEmpty()) {
      ComponentPageElement componentElement = componentQueue.pop();

      loadTemplateForComponent(componentElement);
    }
  }
}
@SuppressWarnings("all")
public class AssetSourceImpl extends LockSupport implements AssetSource {

  private final List<String> EXTERNAL_URL_PREFIXES =
      Arrays.asList(
          AssetConstants.HTTP,
          AssetConstants.HTTPS,
          AssetConstants.PROTOCOL_RELATIVE,
          AssetConstants.FTP);

  private final StrategyRegistry<AssetFactory> registry;

  private final ThreadLocale threadLocale;

  private final Map<String, Resource> prefixToRootResource = CollectionFactory.newMap();

  private final Map<Resource, SoftReference<Asset>> cache = CollectionFactory.newConcurrentMap();

  private final SymbolSource symbolSource;

  private final Logger logger;

  private final AtomicBoolean firstWarning = new AtomicBoolean(true);

  private final OperationTracker tracker;

  private final Request request;

  private final Map<String, AssetFactory> configuration;

  public AssetSourceImpl(
      ThreadLocale threadLocale,
      Map<String, AssetFactory> configuration,
      SymbolSource symbolSource,
      Logger logger,
      OperationTracker tracker) {
    this(threadLocale, configuration, symbolSource, logger, tracker, null);
  }

  public AssetSourceImpl(
      ThreadLocale threadLocale,
      Map<String, AssetFactory> configuration,
      SymbolSource symbolSource,
      Logger logger,
      OperationTracker tracker,
      Request request) {
    this.configuration = configuration;
    this.threadLocale = threadLocale;
    this.symbolSource = symbolSource;
    this.logger = logger;
    this.tracker = tracker;
    this.request = request;

    Map<Class, AssetFactory> byResourceClass = CollectionFactory.newMap();

    for (Map.Entry<String, AssetFactory> e : configuration.entrySet()) {
      String prefix = e.getKey();
      AssetFactory factory = e.getValue();

      Resource rootResource = factory.getRootResource();

      byResourceClass.put(rootResource.getClass(), factory);

      prefixToRootResource.put(prefix, rootResource);
    }

    registry = StrategyRegistry.newInstance(AssetFactory.class, byResourceClass);
  }

  @PostInjection
  public void clearCacheWhenResourcesChange(ResourceChangeTracker tracker) {
    tracker.clearOnInvalidation(cache);
  }

  public Asset getClasspathAsset(String path) {
    return getClasspathAsset(path, null);
  }

  public Asset getClasspathAsset(String path, Locale locale) {
    return getAsset(null, path, locale);
  }

  public Asset getContextAsset(String path, Locale locale) {
    return getAsset(prefixToRootResource.get(AssetConstants.CONTEXT), path, locale);
  }

  public Asset getAsset(Resource baseResource, String path, Locale locale) {
    return getAssetInLocale(baseResource, path, defaulted(locale));
  }

  public Resource resourceForPath(String path) {
    return findResource(null, path);
  }

  public Asset getExpandedAsset(String path) {
    return getUnlocalizedAsset(symbolSource.expandSymbols(path));
  }

  public Asset getComponentAsset(
      final ComponentResources resources, final String path, final String libraryName) {
    assert resources != null;

    assert InternalUtils.isNonBlank(path);

    return tracker.invoke(
        String.format("Resolving '%s' for component %s", path, resources.getCompleteId()),
        new Invokable<Asset>() {
          public Asset invoke() {
            // First, expand symbols:

            String expanded = symbolSource.expandSymbols(path);

            int dotx = expanded.indexOf(':');

            // We special case the hell out of 'classpath:' so that we can provide warnings today
            // (5.4) and
            // blow up in a useful fashion tomorrow (5.5).

            if (expanded.startsWith("//")
                || (dotx > 0
                    && !expanded.substring(0, dotx).equalsIgnoreCase(AssetConstants.CLASSPATH))) {
              final String prefix =
                  dotx >= 0 ? expanded.substring(0, dotx) : AssetConstants.PROTOCOL_RELATIVE;
              if (EXTERNAL_URL_PREFIXES.contains(prefix)) {

                String url;
                if (prefix.equals(AssetConstants.PROTOCOL_RELATIVE)) {
                  url = (request != null && request.isSecure() ? "https:" : "http:") + expanded;
                  url = url.replace("//:", "//");
                } else {
                  url = expanded;
                }

                try {
                  UrlResource resource = new UrlResource(new URL(url));
                  return new UrlAsset(url, resource);
                } catch (MalformedURLException e) {
                  throw new RuntimeException(e);
                }
              } else {
                return getAssetInLocale(
                    resources.getBaseResource(), expanded, resources.getLocale());
              }
            }

            // No prefix, so implicitly classpath:, or explicitly classpath:

            String restOfPath = expanded.substring(dotx + 1);

            // This is tricky, because a relative path (including "../") is ok in 5.3, since its
            // just somewhere
            // else on the classpath (though you can "stray" out of the "safe" zone).  In 5.4, under
            // /META-INF/assets/
            // it's possible to "stray" out beyond the safe zone more easily, into parts of the
            // classpath that can't be
            // represented in the URL.

            // Ends with trailing slash:
            String metaRoot = "META-INF/assets/" + toPathPrefix(libraryName);

            String trimmedRestOfPath =
                restOfPath.startsWith("/") ? restOfPath.substring(1) : restOfPath;

            // TAP5-2044: Some components specify a full path, starting with META-INF/assets/, and
            // we should just trust them.
            // The warning logic below is for compnents that specify a relative path. Our bad
            // decisions come back to haunt us;
            // Resource paths should always had a leading slash to differentiate relative from
            // complete.
            String metaPath =
                trimmedRestOfPath.startsWith("META-INF/assets/")
                    ? trimmedRestOfPath
                    : metaRoot + trimmedRestOfPath;

            // Based on the path, metaResource is where it should exist in a 5.4 and beyond world
            // ... unless the expanded
            // path was a bit too full of ../ sequences, in which case the expanded path is not
            // valid and we adjust the
            // error we write.

            Resource metaResource = findLocalizedResource(null, metaPath, resources.getLocale());

            Asset result = getComponentAsset(resources, expanded, metaResource);

            if (result == null) {
              throw new RuntimeException(
                  String.format(
                      "Unable to locate asset '%s' for component %s. It should be located at %s.",
                      path, resources.getCompleteId(), metaPath));
            }

            // This is the best way to tell if the result is an asset for a Classpath resource.

            Resource resultResource = result.getResource();

            if (!resultResource.equals(metaResource)) {
              if (firstWarning.getAndSet(false)) {
                logger.error(
                    "Packaging of classpath assets has changed in release 5.4; "
                        + "Assets should no longer be on the main classpath, "
                        + "but should be moved to 'META-INF/assets/' or a sub-folder. Future releases of Tapestry may "
                        + "no longer support assets on the main classpath.");
              }

              if (metaResource.getFolder().startsWith(metaRoot)) {
                logger.warn(
                    String.format(
                        "Classpath asset '/%s' should be moved to folder '/%s/'.",
                        resultResource.getPath(), metaResource.getFolder()));
              } else {
                logger.warn(
                    String.format(
                        "Classpath asset '/%s' should be moved under folder '/%s', and the relative path adjusted.",
                        resultResource.getPath(), metaRoot));
              }
            }

            return result;
          }
        });
  }

  private Asset getComponentAsset(
      ComponentResources resources, String expandedPath, Resource metaResource) {

    if (expandedPath.contains(":") || expandedPath.startsWith("/")) {
      return getAssetInLocale(resources.getBaseResource(), expandedPath, resources.getLocale());
    }

    // So, it's relative to the component.  First, check if there's a match using the 5.4 rules.

    if (metaResource.exists()) {
      return getAssetForResource(metaResource);
    }

    Resource oldStyle =
        findLocalizedResource(resources.getBaseResource(), expandedPath, resources.getLocale());

    if (oldStyle == null || !oldStyle.exists()) {
      return null;
    }

    return getAssetForResource(oldStyle);
  }

  /**
   * Figure out the relative path, under /META-INF/assets/ for resources for a given library. The
   * application library is the blank string and goes directly in /assets/; other libraries are like
   * virtual folders within /assets/.
   */
  private String toPathPrefix(String libraryName) {
    return libraryName.equals("") ? "" : libraryName + "/";
  }

  public Asset getUnlocalizedAsset(String path) {
    return getAssetInLocale(null, path, null);
  }

  private Asset getAssetInLocale(Resource baseResource, String path, Locale locale) {
    return getLocalizedAssetFromResource(findResource(baseResource, path), locale);
  }

  /**
   * @param baseResource the base resource (or null for classpath root) that path will extend from
   * @param path extension path from the base resource
   * @return the resource, unlocalized, which may not exist (may be for a path with no actual
   *     resource)
   */
  private Resource findResource(Resource baseResource, String path) {
    assert path != null;
    int colonx = path.indexOf(':');

    if (colonx < 0) {
      Resource root =
          baseResource != null ? baseResource : prefixToRootResource.get(AssetConstants.CLASSPATH);

      return root.forFile(path);
    }

    String prefix = path.substring(0, colonx);

    Resource root = prefixToRootResource.get(prefix);

    if (root == null)
      throw new IllegalArgumentException(
          String.format("Unknown prefix for asset path '%s'.", path));

    return root.forFile(path.substring(colonx + 1));
  }

  /**
   * Finds a localized resource.
   *
   * @param baseResource base resource, or null for classpath root
   * @param path path from baseResource to expected resource
   * @param locale locale to localize for, or null to not localize
   * @return resource, which may not exist
   */
  private Resource findLocalizedResource(Resource baseResource, String path, Locale locale) {
    Resource unlocalized = findResource(baseResource, path);

    if (locale == null || !unlocalized.exists()) {
      return unlocalized;
    }

    return localize(unlocalized, locale);
  }

  private Resource localize(Resource unlocalized, Locale locale) {
    Resource localized = unlocalized.forLocale(locale);

    return localized != null ? localized : unlocalized;
  }

  private Asset getLocalizedAssetFromResource(Resource unlocalized, Locale locale) {
    Resource localized = locale == null ? unlocalized : unlocalized.forLocale(locale);

    if (localized == null || !localized.exists())
      throw new RuntimeException(
          String.format("Unable to locate asset '%s' (the file does not exist).", unlocalized));

    return getAssetForResource(localized);
  }

  private Asset getAssetForResource(Resource resource) {
    try {
      acquireReadLock();

      Asset result = TapestryInternalUtils.getAndDeref(cache, resource);

      if (result == null) {
        result = createAssetFromResource(resource);
        cache.put(resource, new SoftReference(result));
      }

      return result;
    } finally {
      releaseReadLock();
    }
  }

  private Locale defaulted(Locale locale) {
    return locale != null ? locale : threadLocale.getLocale();
  }

  private Asset createAssetFromResource(Resource resource) {
    // The class of the resource is derived from the class of the base resource.
    // So we can then use the class of the resource as a key to locate the correct asset
    // factory.

    try {
      upgradeReadLockToWriteLock();

      // Check for competing thread beat us to it (not very likely!):

      Asset result = TapestryInternalUtils.getAndDeref(cache, resource);

      if (result != null) {
        return result;
      }

      Class resourceClass = resource.getClass();

      AssetFactory factory = registry.get(resourceClass);

      return factory.createAsset(resource);
    } finally {
      downgradeWriteLockToReadLock();
    }
  }
}