001// Copyright 2006, 2008, 2010, 2011, 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.ioc.internal.util;
016
017import org.apache.tapestry5.ioc.Resource;
018import org.apache.tapestry5.ioc.util.LocalizedNameGenerator;
019
020import java.io.BufferedInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.URL;
024import java.util.Locale;
025
026/**
027 * Abstract implementation of {@link Resource}. Subclasses must implement the abstract methods {@link Resource#toURL()}
028 * and {@link #newResource(String)} as well as toString(), hashCode() and equals().
029 */
030public abstract class AbstractResource extends LockSupport implements Resource
031{
032    private class Localization
033    {
034        final Locale locale;
035
036        final Resource resource;
037
038        final Localization next;
039
040        private Localization(Locale locale, Resource resource, Localization next)
041        {
042            this.locale = locale;
043            this.resource = resource;
044            this.next = next;
045        }
046    }
047
048    private final String path;
049
050    // Guarded by Lock
051    private boolean exists, existsComputed;
052
053    // Guarded by lock
054    private Localization firstLocalization;
055
056    protected AbstractResource(String path)
057    {
058        assert path != null;
059        this.path = path;
060    }
061
062    public final String getPath()
063    {
064        return path;
065    }
066
067    public final String getFile()
068    {
069        int slashx = path.lastIndexOf('/');
070
071        return path.substring(slashx + 1);
072    }
073
074    public final String getFolder()
075    {
076        int slashx = path.lastIndexOf('/');
077
078        return (slashx < 0) ? "" : path.substring(0, slashx);
079    }
080
081    public final Resource forFile(String relativePath)
082    {
083        assert relativePath != null;
084        StringBuilder builder = new StringBuilder(getFolder());
085
086        for (String term : relativePath.split("/"))
087        {
088            // This will occur if the relative path contains sequential slashes
089
090            if (term.equals(""))
091                continue;
092
093            if (term.equals("."))
094                continue;
095
096            if (term.equals(".."))
097            {
098                int slashx = builder.lastIndexOf("/");
099
100                // TODO: slashx < 0 (i.e., no slash)
101
102                // Trim path to content before the slash
103
104                builder.setLength(slashx);
105                continue;
106            }
107
108            // TODO: term blank or otherwise invalid?
109            // TODO: final term should not be "." or "..", or for that matter, the
110            // name of a folder, since a Resource should be a file within
111            // a folder.
112
113            if (builder.length() > 0)
114                builder.append("/");
115
116            builder.append(term);
117        }
118
119        return createResource(builder.toString());
120    }
121
122    public final Resource forLocale(Locale locale)
123    {
124        try
125        {
126            acquireReadLock();
127
128            for (Localization l = firstLocalization; l != null; l = l.next)
129            {
130                if (l.locale.equals(locale))
131                {
132                    return l.resource;
133                }
134            }
135
136            return populateLocalizationCache(locale);
137        } finally
138        {
139            releaseReadLock();
140        }
141    }
142
143    private Resource populateLocalizationCache(Locale locale)
144    {
145        try
146        {
147            upgradeReadLockToWriteLock();
148
149            // Race condition: another thread may have beaten us to it:
150
151            for (Localization l = firstLocalization; l != null; l = l.next)
152            {
153                if (l.locale.equals(locale))
154                {
155                    return l.resource;
156                }
157            }
158
159            Resource result = findLocalizedResource(locale);
160
161            firstLocalization = new Localization(locale, result, firstLocalization);
162
163            return result;
164
165        } finally
166        {
167            downgradeWriteLockToReadLock();
168        }
169    }
170
171    private Resource findLocalizedResource(Locale locale)
172    {
173        for (String path : new LocalizedNameGenerator(this.path, locale))
174        {
175            Resource potential = createResource(path);
176
177            if (potential.exists())
178                return potential;
179        }
180
181        return null;
182    }
183
184    public final Resource withExtension(String extension)
185    {
186        assert InternalUtils.isNonBlank(extension);
187        int dotx = path.lastIndexOf('.');
188
189        if (dotx < 0)
190            return createResource(path + "." + extension);
191
192        return createResource(path.substring(0, dotx + 1) + extension);
193    }
194
195    /**
196     * Creates a new resource, unless the path matches the current Resource's path (in which case, this resource is
197     * returned).
198     */
199    private Resource createResource(String path)
200    {
201        if (this.path.equals(path))
202            return this;
203
204        return newResource(path);
205    }
206
207    /**
208     * Simple check for whether {@link #toURL()} returns null or not.
209     */
210    public boolean exists()
211    {
212        try
213        {
214            acquireReadLock();
215
216            if (!existsComputed)
217            {
218                computeExists();
219            }
220
221            return exists;
222        } finally
223        {
224            releaseReadLock();
225        }
226    }
227
228    private void computeExists()
229    {
230        try
231        {
232            upgradeReadLockToWriteLock();
233
234            if (!existsComputed)
235            {
236                exists = toURL() != null;
237                existsComputed = true;
238            }
239        } finally
240        {
241            downgradeWriteLockToReadLock();
242        }
243    }
244
245    /**
246     * Obtains the URL for the Resource and opens the stream, wrapped by a BufferedInputStream.
247     */
248    public InputStream openStream() throws IOException
249    {
250        URL url = toURL();
251
252        if (url == null)
253            return null;
254
255        return new BufferedInputStream(url.openStream());
256    }
257
258    /**
259     * Factory method provided by subclasses.
260     */
261    protected abstract Resource newResource(String path);
262}