001// Copyright 2012 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.internal.util;
016
017import org.apache.tapestry5.commons.Messages;
018import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
019import org.apache.tapestry5.json.JSONObject;
020import org.apache.tapestry5.services.messages.ComponentMessagesSource;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.URL;
025import java.util.Locale;
026
027public class MessageCatalogResource extends VirtualResource
028{
029    private final Locale locale;
030
031    private final ComponentMessagesSource messagesSource;
032
033    private final boolean compactJSON;
034
035    private volatile byte[] bytes;
036
037    public MessageCatalogResource(Locale locale, ComponentMessagesSource messagesSource, final ResourceChangeTracker changeTracker, boolean compactJSON)
038    {
039        this.locale = locale;
040        this.messagesSource = messagesSource;
041        this.compactJSON = compactJSON;
042
043        messagesSource.getInvalidationEventHub().addInvalidationCallback(new Runnable()
044        {
045            public void run()
046            {
047                bytes = null;
048
049                // What's all this then?  This MessageCatalogResource is converted, as if it was a real file, into
050                // a StreamableResource by the StreamableResourceService service; as a virtual resource, we don't have
051                // any date-time-modified information for the application message catalog (as if that was possible,
052                // given that its composed of multiple files). The ComponentMessagesSource can tell us when
053                // *some* properties file has changed (not necessarily one used in the application message catalog,
054                // but that's the breaks). When that occurs, we tell the ResourceChangeTracker to fire its invalidation
055                // event. That flushes out all the assets it has cached, including StreamableResources for JavaScript files,
056                // including the one created here to represent the application message catalog.
057                
058                // TAP-2742: now we're doing smarter page cache invalidation,
059                // so we don't need to clear everything anymore.
060                
061                // changeTracker.forceInvalidationEvent();
062            }
063        });
064    }
065
066    @Override
067    public String toString()
068    {
069        return String.format("MessageCatalogResource[%s]", locale);
070    }
071
072    /**
073     * StreamableResourceSourceImpl needs a file, and a suffix, to determine the content type.
074     */
075    @Override
076    public String getFile()
077    {
078        return String.format("virtual-%s.js", locale);
079    }
080
081    /**
082     * To work as a streamable resource, must return a value (or null) from toURL.
083     */
084    @Override
085    public URL toURL()
086    {
087        return null;
088    }
089
090    public InputStream openStream() throws IOException
091    {
092        return toInputStream(getBytes());
093    }
094
095    private byte[] getBytes() throws IOException
096    {
097        if (bytes == null)
098        {
099            bytes = assembleCatalog().getBytes(UTF8);
100        }
101
102        return bytes;
103    }
104
105    private String assembleCatalog()
106    {
107        Messages messages = messagesSource.getApplicationCatalog(locale);
108
109        JSONObject catalog = new JSONObject();
110
111        for (String key : messages.getKeys())
112        {
113            if (key.startsWith("private-"))
114            {
115                continue;
116            }
117
118            String value = messages.get(key);
119
120            if (value.contains("%"))
121            {
122                continue;
123            }
124
125            catalog.put(key, value);
126        }
127
128        StringBuilder builder = new StringBuilder(2000);
129
130        builder.append("define(").append(catalog.toString(compactJSON)).append(");");
131
132        return builder.toString();
133    }
134}