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.request.mapper.info;
018
019import org.apache.wicket.util.lang.Args;
020import org.apache.wicket.util.string.Strings;
021
022/**
023 * Encodes listener and component path in form of
024 * {@code <listener>-<componentPath>},
025 * {@code <listener>.<behaviorIndex>-<componentPath>} or
026 * {@code <render-count>.<listener>.<behaviorIndex>-<componentPath>}
027 * <p>
028 * Component path is escaped (':' characters are replaced by '~')
029 * 
030 * @author Matej Knopp
031 */
032public class ComponentInfo
033{
034        private static final char BEHAVIOR_INDEX_SEPARATOR = '.';
035        private static final char SEPARATOR = '-';
036        private static final char COMPONENT_SEPARATOR = ':';
037        private static final char SEPARATOR_ENCODED = '~';
038
039        /**
040         * Replaces ':' with '-', and '-' with '~'.
041         * 
042         * @param path
043         *            the path to the component in its page
044         * @return the encoded path
045         */
046        private static String encodeComponentPath(CharSequence path)
047        {
048                if (path != null)
049                {
050                        int length = path.length();
051                        if (length == 0)
052                        {
053                                return path.toString();
054                        }
055                        StringBuilder result = new StringBuilder(length);
056                        for (int i = 0; i < length; i++)
057                        {
058                                char c = path.charAt(i);
059                                switch (c)
060                                {
061                                        case COMPONENT_SEPARATOR :
062                                                result.append(SEPARATOR);
063                                                break;
064                                        case SEPARATOR :
065                                                result.append(SEPARATOR_ENCODED);
066                                                break;
067                                        default :
068                                                result.append(c);
069                                }
070                        }
071                        return result.toString();
072                }
073                else
074                {
075                        return null;
076                }
077        }
078
079        /**
080         * Replaces '~' with '-' and '-' with ':'
081         * 
082         * @param path
083         *            the encoded path of the component in its page
084         * @return the (non-encoded) path of the component in its page
085         */
086        private static String decodeComponentPath(CharSequence path)
087        {
088                if (path != null)
089                {
090                        int length = path.length();
091                        if (length == 0)
092                        {
093                                return path.toString();
094                        }
095                        StringBuilder result = new StringBuilder(length);
096                        for (int i = 0; i < length; i++)
097                        {
098                                char c = path.charAt(i);
099                                switch (c)
100                                {
101                                        case SEPARATOR_ENCODED :
102                                                result.append(SEPARATOR);
103                                                break;
104                                        case SEPARATOR :
105                                                result.append(COMPONENT_SEPARATOR);
106                                                break;
107                                        default :
108                                                result.append(c);
109                                }
110                        }
111                        return result.toString();
112                }
113                else
114                {
115                        return null;
116                }
117        }
118
119        private final String componentPath;
120        private final Integer behaviorId;
121        private final Integer renderCount;
122
123        /**
124         * Construct.
125         * 
126         * @param renderCount
127         * @param componentPath
128         * @param behaviorId
129         */
130        public ComponentInfo(final Integer renderCount, final String componentPath, final Integer behaviorId)
131        {
132                Args.notNull(componentPath, "componentPath");
133
134                this.componentPath = componentPath;
135                this.behaviorId = behaviorId;
136                this.renderCount = renderCount;
137        }
138
139        /**
140         * @return component path
141         */
142        public String getComponentPath()
143        {
144                return componentPath;
145        }
146
147        /**
148         * @return behavior index
149         */
150        public Integer getBehaviorId()
151        {
152                return behaviorId;
153        }
154
155        /**
156         * 
157         * @return render count
158         */
159        public Integer getRenderCount()
160        {
161                return renderCount;
162        }
163
164        /**
165         * @see java.lang.Object#toString()
166         */
167        @Override
168        public String toString()
169        {
170                String path = encodeComponentPath(componentPath);
171                StringBuilder result = new StringBuilder(path.length() + 12);
172
173                if (renderCount != null)
174                {
175                        result.append(renderCount);
176                }
177
178                if (renderCount != null || behaviorId != null) {
179                        result.append(BEHAVIOR_INDEX_SEPARATOR);
180                }
181                
182                if (behaviorId != null)
183                {
184                        result.append(behaviorId);
185                }
186                result.append(SEPARATOR);
187                result.append(path);
188
189                return result.toString();
190        }
191
192        /**
193         * Method that rigidly checks if the string consists of digits only.
194         * 
195         * @param string
196         * @return whether the string consists of digits only
197         */
198        private static boolean isNumber(final String string)
199        {
200                if (string == null || string.isEmpty())
201                {
202                        return false;
203                }
204                for (int i = 0; i < string.length(); ++i)
205                {
206                        if (!Character.isDigit(string.charAt(i)))
207                        {
208                                return false;
209                        }
210                }
211                return true;
212        }
213
214        /**
215         * Parses the given string.
216         * 
217         * @param string
218         * @return component info or <code>null</code> if the string is not in correct format.
219         */
220        public static ComponentInfo parse(final String string)
221        {
222                if (Strings.isEmpty(string))
223                {
224                        return null;
225                }
226                int i = string.indexOf(SEPARATOR);
227                if (i == -1)
228                {
229                        return null;
230                }
231                else
232                {
233                        String listener = string.substring(0, i);
234                        String componentPath = decodeComponentPath(string.substring(i + 1));
235
236                        Integer behaviorIndex = null;
237                        Integer renderCount = null;
238
239                        String listenerParts[] = Strings.split(listener, BEHAVIOR_INDEX_SEPARATOR);
240                        if (listenerParts.length == 0)
241                        {
242                                return new ComponentInfo(renderCount, componentPath, behaviorIndex);
243                        }
244                        else if (listenerParts.length == 2)
245                        {
246                                if (isNumber(listenerParts[0]))
247                                {
248                                        renderCount = Integer.valueOf(listenerParts[0]);
249                                }
250                                if (isNumber(listenerParts[1]))
251                                {
252                                        behaviorIndex = Integer.valueOf(listenerParts[1]);
253                                }
254                                
255                                return new ComponentInfo(renderCount, componentPath, behaviorIndex);
256                        }
257                        else
258                        {
259                                return null;
260                        }
261                }
262        }
263}