001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.time.Instant;
022import java.util.Locale;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025import org.apache.wicket.Component;
026import org.apache.wicket.core.util.lang.WicketObjects;
027import org.apache.wicket.util.lang.Args;
028import org.apache.wicket.util.lang.Bytes;
029import org.apache.wicket.util.resource.IFixedLocationResourceStream;
030import org.apache.wicket.util.resource.IResourceStream;
031import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
032import org.apache.wicket.util.string.Strings;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036
037/**
038 * An IResourceStream implementation with specific extensions for markup resource streams.
039 * 
040 * @author Juergen Donnerstag
041 */
042public class MarkupResourceStream implements IResourceStream, IFixedLocationResourceStream
043{
044        private static final long serialVersionUID = 1846489965076612828L;
045
046        private static final Logger log = LoggerFactory.getLogger(MarkupResourceStream.class);
047
048        /** */
049        public static final String WICKET_XHTML_DTD = "http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd";
050
051        private static final Pattern DOCTYPE_REGEX = Pattern.compile("!DOCTYPE\\s+(.*)\\s*");
052
053        /** The associated markup resource stream */
054        private final IResourceStream resourceStream;
055
056        /** Container info like Class, locale and style which were used to locate the resource */
057        private final transient ContainerInfo containerInfo;
058
059        /**
060         * The actual component class the markup is directly associated with. It might be super class of
061         * the component class
062         */
063        private final String markupClassName;
064
065        /** The key used to cache the markup resource stream */
066        private String cacheKey;
067
068        /** In case of the inherited markup, this is the base markup */
069        private transient Markup baseMarkup;
070
071        /** The encoding as found in <?xml ... encoding="" ?>. {@code null}, otherwise */
072        private String encoding;
073
074        /** Wicket namespace: see WICKET_XHTML_DTD */
075        private String wicketNamespace;
076
077        /** == wicket namespace name + ":id" */
078        private String wicketId;
079
080        /** HTML5 http://www.w3.org/TR/html5-diff/#doctype */
081        private String doctype;
082
083        /**
084         * Construct.
085         * 
086         * @param resourceStream
087         */
088        public MarkupResourceStream(final IResourceStream resourceStream)
089        {
090                this(resourceStream, null, null);
091        }
092
093        /**
094         * Construct.
095         * 
096         * @param resourceStream
097         * @param containerInfo
098         * @param markupClass
099         */
100        public MarkupResourceStream(final IResourceStream resourceStream,
101                final ContainerInfo containerInfo, final Class<?> markupClass)
102        {
103                this.resourceStream = Args.notNull(resourceStream, "resourceStream");
104                this.containerInfo = containerInfo;
105                markupClassName = markupClass == null ? null : markupClass.getName();
106
107                setWicketNamespace(MarkupParser.WICKET);
108        }
109
110        @Override
111        public String locationAsString()
112        {
113                if (resourceStream instanceof IFixedLocationResourceStream)
114                {
115                        return ((IFixedLocationResourceStream)resourceStream).locationAsString();
116                }
117                return null;
118        }
119
120        @Override
121        public void close() throws IOException
122        {
123                resourceStream.close();
124        }
125
126        @Override
127        public String getContentType()
128        {
129                return resourceStream.getContentType();
130        }
131
132        @Override
133        public InputStream getInputStream() throws ResourceStreamNotFoundException
134        {
135                return resourceStream.getInputStream();
136        }
137
138        @Override
139        public Locale getLocale()
140        {
141                return resourceStream.getLocale();
142        }
143
144        @Override
145        public Instant lastModifiedTime()
146        {
147                return resourceStream.lastModifiedTime();
148        }
149
150        @Override
151        public Bytes length()
152        {
153                return resourceStream.length();
154        }
155
156        @Override
157        public void setLocale(Locale locale)
158        {
159                resourceStream.setLocale(locale);
160        }
161
162        /**
163         * Get the actual component class the markup is directly associated with. Note: it not
164         * necessarily must be the container class.
165         * 
166         * @return The directly associated class
167         */
168        public Class<? extends Component> getMarkupClass()
169        {
170                if (markupClassName == null)
171                {
172                        throw new MarkupException("no associated markup class");
173                }
174                return WicketObjects.resolveClass(markupClassName);
175        }
176
177        /**
178         * Get the container info associated with the markup
179         * 
180         * @return ContainerInfo
181         */
182        public ContainerInfo getContainerInfo()
183        {
184                return containerInfo;
185        }
186
187        /**
188         * Gets cacheKey.
189         * 
190         * @return cacheKey
191         */
192        public final String getCacheKey()
193        {
194                return cacheKey;
195        }
196
197        /**
198         * Set the cache key
199         * 
200         * @param cacheKey
201         */
202        public final void setCacheKey(final String cacheKey)
203        {
204                this.cacheKey = cacheKey;
205        }
206
207        /**
208         * Gets the resource that contains this markup
209         * 
210         * @return The resource where this markup came from
211         */
212        public IResourceStream getResource()
213        {
214                return resourceStream;
215        }
216
217        /**
218         * Gets the markup encoding. A markup encoding may be specified in a markup file with an XML
219         * encoding specifier of the form &lt;?xml ... encoding="..." ?&gt;.
220         * 
221         * @return Encoding, or null if not found.
222         */
223        public String getEncoding()
224        {
225                return encoding;
226        }
227
228        /**
229         * Get the wicket namespace valid for this specific markup
230         * 
231         * @return wicket namespace
232         */
233        public String getWicketNamespace()
234        {
235                return wicketNamespace;
236        }
237
238        /**
239         * 
240         * @return usually it is "wicket:id"
241         */
242        final public String getWicketId()
243        {
244                return wicketId;
245        }
246
247        /**
248         * Sets encoding.
249         * 
250         * @param encoding
251         *            encoding
252         */
253        final void setEncoding(final String encoding)
254        {
255                this.encoding = encoding;
256        }
257
258        /**
259         * Sets wicketNamespace.
260         * 
261         * @param wicketNamespace
262         *            wicketNamespace
263         */
264        public final void setWicketNamespace(final String wicketNamespace)
265        {
266                this.wicketNamespace = wicketNamespace;
267                wicketId = (wicketNamespace + ":id").intern();
268
269                if (!MarkupParser.WICKET.equals(wicketNamespace) && log.isDebugEnabled())
270                {
271                        log.debug("You are using a non-standard namespace name: '{}'", wicketNamespace);
272                }
273        }
274
275        /**
276         * Get the resource stream containing the base markup (markup inheritance)
277         * 
278         * @return baseMarkupResource Null, if not base markup
279         */
280        public MarkupResourceStream getBaseMarkupResourceStream()
281        {
282                if (baseMarkup == null)
283                {
284                        return null;
285                }
286                return baseMarkup.getMarkupResourceStream();
287        }
288
289        /**
290         * In case of markup inheritance, the base markup.
291         * 
292         * @param baseMarkup
293         *            The base markup
294         */
295        public void setBaseMarkup(Markup baseMarkup)
296        {
297                this.baseMarkup = baseMarkup;
298        }
299
300        /**
301         * In case of markup inheritance, the base markup resource.
302         * 
303         * @return The base markup
304         */
305        public Markup getBaseMarkup()
306        {
307                return baseMarkup;
308        }
309
310        @Override
311        public String getStyle()
312        {
313                return resourceStream.getStyle();
314        }
315
316        @Override
317        public String getVariation()
318        {
319                return resourceStream.getVariation();
320        }
321
322        @Override
323        public void setStyle(String style)
324        {
325                resourceStream.setStyle(style);
326        }
327
328        @Override
329        public void setVariation(String variation)
330        {
331                resourceStream.setVariation(variation);
332        }
333
334        @Override
335        public String toString()
336        {
337                if (resourceStream != null)
338                {
339                        return resourceStream.toString();
340                }
341                else
342                {
343                        return "(unknown resource)";
344                }
345        }
346
347        /**
348         * Gets doctype.
349         * 
350         * @return The doctype excluding 'DOCTYPE'
351         */
352        public final String getDoctype()
353        {
354                if (doctype == null)
355                {
356                        MarkupResourceStream baseMarkupResourceStream = getBaseMarkupResourceStream();
357                        if (baseMarkupResourceStream != null)
358                        {
359                                doctype = baseMarkupResourceStream.getDoctype();
360                        }
361                }
362
363                return doctype;
364        }
365
366        /**
367         * Sets doctype.
368         * 
369         * @param doctype
370         *            doctype
371         */
372        public final void setDoctype(final CharSequence doctype)
373        {
374                if (Strings.isEmpty(doctype) == false)
375                {
376                        String doc = doctype.toString().replaceAll("[\n\r]+", "");
377                        doc = doc.replaceAll("\\s+", " ");
378                        Matcher matcher = DOCTYPE_REGEX.matcher(doc);
379                        if (matcher.matches() == false)
380                        {
381                                throw new MarkupException("Invalid DOCTYPE: '" + doctype + "'");
382                        }
383                        this.doctype = matcher.group(1).trim();
384                }
385        }
386
387        /**
388         * @see <a href="http://www.w3.org/TR/html5-diff/#doctype">DOCTYPE</a>
389         * @return True, if doctype == &lt;!DOCTYPE html&gt;
390         */
391        public boolean isHtml5()
392        {
393                return "html".equalsIgnoreCase(getDoctype());
394        }
395}