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.parser.filter;
018
019import java.text.ParseException;
020import java.util.ArrayDeque;
021
022import org.apache.wicket.Application;
023import org.apache.wicket.Component;
024import org.apache.wicket.MarkupContainer;
025import org.apache.wicket.WicketRuntimeException;
026import org.apache.wicket.markup.ComponentTag;
027import org.apache.wicket.markup.MarkupElement;
028import org.apache.wicket.markup.MarkupResourceStream;
029import org.apache.wicket.markup.MarkupStream;
030import org.apache.wicket.markup.WicketTag;
031import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
032import org.apache.wicket.markup.parser.AbstractMarkupFilter;
033import org.apache.wicket.markup.resolver.IComponentResolver;
034import org.apache.wicket.util.string.StringValueConversionException;
035import org.apache.wicket.util.string.Strings;
036import org.apache.wicket.util.value.IValueMap;
037
038
039/**
040 * This is a markup inline filter. It identifies xml tags which include a href attribute and which
041 * are not Wicket specific components and flags these tags (ComponentTag) as autolink enabled. A
042 * component resolver will later resolve the href and assign a BookmarkablePageLink to it
043 * (automatically).
044 * <p>
045 * An application setting is used as default value, which might be modified for specific regions.
046 * These regions are identified by &lt;wicket:link&gt; tags with an optional 'autolink' attribute.
047 * The default value for the attribute is true, thus enabling autolinking. An open-close
048 * &lt;wicket:link/&gt; tag will change the autolink status until the end of the markup document or
049 * the next &lt;wicket:link&gt; tag respectively. &lt;wicket:link&gt; regions may be nested.
050 * 
051 * @author Juergen Donnerstag
052 */
053public class WicketLinkTagHandler extends AbstractMarkupFilter implements IComponentResolver
054{
055        private static final long serialVersionUID = 1L;
056
057        /** */
058        public static final String LINK = "link";
059
060        /** The id of autolink components */
061        public static final String AUTOLINK_ID = "_autolink_";
062
063        /** Allow to have link regions within link regions */
064        private ArrayDeque<Boolean> autolinkStatus;
065
066        /** Current status */
067        private boolean autolinking = true;
068
069        /**
070         * Construct.
071         */
072        public WicketLinkTagHandler()
073        {
074                this(null);
075        }
076
077        public WicketLinkTagHandler(MarkupResourceStream resourceStream)
078        {
079                super(resourceStream);
080                setAutomaticLinking(Application.get().getMarkupSettings().getAutomaticLinking());
081        }
082
083        /**
084         * Set the default value for autolinking
085         * 
086         * @param enable
087         *            if true, autolinks are enabled
088         */
089        public void setAutomaticLinking(final boolean enable)
090        {
091                autolinking = enable;
092        }
093
094        @Override
095        protected final MarkupElement onComponentTag(ComponentTag tag) throws ParseException
096        {
097                // Only xml tags not already identified as Wicket components will be
098                // considered for autolinking. This is because it is assumed that Wicket
099                // components like images or all other kind of Wicket Links will handle
100                // it themselves.
101                // Subclass analyzeAutolinkCondition() to implement your own
102                // implementation and register the new tag handler with the markup
103                // parser through Application.newMarkupParser().
104                if ((autolinking == true) && (analyzeAutolinkCondition(tag) == true))
105                {
106                        // Mark it as autolink enabled
107                        tag.enableAutolink(true);
108
109                        // Just a dummy name. The ComponentTag will not be forwarded.
110                        tag.setId(AUTOLINK_ID + getRequestUniqueId());
111                        tag.setAutoComponentTag(true);
112                        tag.setModified(true);
113                        return tag;
114                }
115
116                // For all <wicket:link ..> tags which probably change the
117                // current autolink status.
118                if (tag instanceof WicketTag)
119                {
120                        final WicketTag wtag = (WicketTag)tag;
121                        if (wtag.isLinkTag())
122                        {
123                                // Beginning of the region
124                                if (tag.isOpen() || tag.isOpenClose())
125                                {
126                                        if (tag.isOpen())
127                                        {
128                                                if (autolinkStatus == null)
129                                                {
130                                                        autolinkStatus = new ArrayDeque<>();
131                                                }
132
133                                                // remember the current setting to be reset after the
134                                                // region
135                                                autolinkStatus.push(autolinking);
136                                        }
137
138                                        // html allows to represent true in different ways
139                                        final String autolink = tag.getAttributes().getString("autolink");
140                                        try
141                                        {
142                                                autolinking = Strings.isEmpty(autolink) || Strings.isTrue(autolink);
143                                        }
144                                        catch (StringValueConversionException e)
145                                        {
146                                                throw new WicketRuntimeException("Invalid autolink attribute value \"" +
147                                                        autolink + "\"");
148                                        }
149                                }
150                                else if (tag.isClose())
151                                {
152                                        // restore the autolink setting from before the region
153                                        autolinking = autolinkStatus.pop();
154                                }
155
156                                return wtag;
157                        }
158                }
159
160                return tag;
161        }
162
163        /**
164         * Analyze the tag. If return value == true, a autolink component will be created.
165         * <p>
166         * Subclass analyzeAutolinkCondition() to implement you own implementation and register the new
167         * tag handler with the markup parser through Application.newMarkupParser().
168         * 
169         * @param tag
170         *            The current tag being parsed
171         * @return If true, tag will become auto-component
172         */
173        protected boolean analyzeAutolinkCondition(final ComponentTag tag)
174        {
175                if (tag.getId() == null)
176                {
177                        IValueMap attributes = tag.getAttributes();
178                        String ref = attributes.getString("href");
179                        if (checkRef(ref))
180                        {
181                                return true;
182                        }
183                        ref = attributes.getString("src");
184                        if (checkRef(ref))
185                        {
186                                return true;
187                        }
188                }
189
190                return false;
191        }
192
193        /**
194         * 
195         * @param ref
196         * @return true if ref is not null and does not contain namespace
197         */
198        private boolean checkRef(String ref)
199        {
200                return (ref != null) && (!ref.contains(":"));
201        }
202
203        @Override
204        public Component resolve(final MarkupContainer container, final MarkupStream markupStream,
205                final ComponentTag tag)
206        {
207                if (tag instanceof WicketTag)
208                {
209                        WicketTag wtag = (WicketTag)tag;
210                        if (wtag.isLinkTag() && (wtag.getNamespace() != null))
211                        {
212                                String id = tag.getId();
213
214                                return new TransparentWebMarkupContainer(id);
215                        }
216                }
217
218                // We were not able to handle the tag
219                return null;
220        }
221}