001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.transform;
014
015import org.apache.tapestry5.EventConstants;
016import org.apache.tapestry5.ValueEncoder;
017import org.apache.tapestry5.annotations.ActivationRequestParameter;
018import org.apache.tapestry5.commons.internal.util.TapestryException;
019import org.apache.tapestry5.http.Link;
020import org.apache.tapestry5.http.services.Request;
021import org.apache.tapestry5.internal.services.ComponentClassCache;
022import org.apache.tapestry5.ioc.util.IdAllocator;
023import org.apache.tapestry5.model.MutableComponentModel;
024import org.apache.tapestry5.plastic.FieldHandle;
025import org.apache.tapestry5.plastic.PlasticClass;
026import org.apache.tapestry5.plastic.PlasticField;
027import org.apache.tapestry5.runtime.Component;
028import org.apache.tapestry5.runtime.ComponentEvent;
029import org.apache.tapestry5.services.ComponentEventHandler;
030import org.apache.tapestry5.services.URLEncoder;
031import org.apache.tapestry5.services.ValueEncoderSource;
032import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
033import org.apache.tapestry5.services.transform.TransformationSupport;
034
035/**
036 * Hooks the activate event handler on the component (presumably, a page) to
037 * extract query parameters, and hooks the link decoration events to extract values
038 * and add them to the {@link Link}.
039 *
040 * @see ActivationRequestParameter
041 * @since 5.2.0
042 */
043@SuppressWarnings("all")
044public class ActivationRequestParameterWorker implements ComponentClassTransformWorker2
045{
046    private final Request request;
047
048    private final ComponentClassCache classCache;
049
050    private final ValueEncoderSource valueEncoderSource;
051
052    private final URLEncoder urlEncoder;
053
054    public ActivationRequestParameterWorker(Request request, ComponentClassCache classCache,
055                                            ValueEncoderSource valueEncoderSource, URLEncoder urlEncoder)
056    {
057        this.request = request;
058        this.classCache = classCache;
059        this.valueEncoderSource = valueEncoderSource;
060        this.urlEncoder = urlEncoder;
061    }
062
063    public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
064    {
065        for (PlasticField field : plasticClass.getFieldsWithAnnotation(ActivationRequestParameter.class))
066        {
067            mapFieldToQueryParameter(field, support);
068        }
069    }
070
071    private void mapFieldToQueryParameter(PlasticField field, TransformationSupport support)
072    {
073        ActivationRequestParameter annotation = field.getAnnotation(ActivationRequestParameter.class);
074
075        String parameterName = getParameterName(field, annotation);
076
077        // Assumption: the field type is not one that's loaded by the component class loader, so it's safe
078        // to convert to a hard type during class transformation.
079
080        Class fieldType = classCache.forName(field.getTypeName());
081
082        ValueEncoder encoder = valueEncoderSource.getValueEncoder(fieldType);
083
084        FieldHandle handle = field.getHandle();
085
086        String fieldName = String.format("%s.%s", field.getPlasticClass().getClassName(), field.getName());
087
088        setValueFromInitializeEventHandler(support, fieldName, annotation.required(), handle, parameterName, encoder, urlEncoder);
089
090        decorateLinks(support, fieldName, handle, parameterName, encoder, urlEncoder);
091
092        preallocateName(support, parameterName);
093    }
094
095
096    private static void preallocateName(TransformationSupport support, final String parameterName)
097    {
098        ComponentEventHandler handler = new ComponentEventHandler()
099        {
100            public void handleEvent(Component instance, ComponentEvent event)
101            {
102                IdAllocator idAllocator = event.getEventContext().get(IdAllocator.class, 0);
103
104                idAllocator.allocateId(parameterName);
105            }
106        };
107
108        support.addEventHandler(EventConstants.PREALLOCATE_FORM_CONTROL_NAMES, 1,
109                "ActivationRequestParameterWorker preallocate form control name '" + parameterName + "' event handler",
110                handler);
111    }
112
113    @SuppressWarnings("all")
114    private void setValueFromInitializeEventHandler(final TransformationSupport support, final String fieldName, final boolean required, final FieldHandle handle,
115                                                    final String parameterName, final ValueEncoder encoder, final URLEncoder urlEncoder)
116    {
117        ComponentEventHandler handler = new ComponentEventHandler()
118        {
119            public void handleEvent(Component instance, ComponentEvent event)
120            {
121                String clientValue = request.getParameter(parameterName);
122
123                if (clientValue == null)
124                {
125                    if (required)
126                    {
127                        throw new TapestryException(String.format("Activation request parameter field %s is marked as required, but query parameter '%s' is null.",
128                                fieldName,
129                                parameterName), null);
130                    }
131
132                    return;
133                }
134
135                // TAP5-1768: unescape encoded value
136                clientValue = urlEncoder.decode(clientValue);
137
138                Object value = encoder.toValue(clientValue);
139
140                handle.set(instance, value);
141            }
142        };
143
144        support.addEventHandler(EventConstants.ACTIVATE, 0,
145                String.format("Restoring field %s from query parameter '%s'", fieldName, parameterName), handler);
146
147    }
148
149    @SuppressWarnings("all")
150    private static void decorateLinks(TransformationSupport support, String fieldName, final FieldHandle handle,
151                                      final String parameterName, final ValueEncoder encoder, final URLEncoder urlEncoder)
152    {
153        ComponentEventHandler handler = new ComponentEventHandler()
154        {
155            public void handleEvent(Component instance, ComponentEvent event)
156            {
157                Object value = handle.get(instance);
158
159                if (value == null)
160                {
161                    return;
162                }
163
164                Link link = event.getEventContext().get(Link.class, 0);
165
166                String clientValue = encoder.toClient(value);
167
168                // TAP5-1768: escape special characters
169                clientValue = urlEncoder.encode(clientValue);
170
171                link.addParameter(parameterName, clientValue);
172            }
173        };
174
175        support.addEventHandler(EventConstants.DECORATE_COMPONENT_EVENT_LINK, 0,
176                String.format("ActivationRequestParameterWorker decorate component event link event handler for field %s as query parameter '%s'", fieldName, parameterName), handler);
177
178        support.addEventHandler(EventConstants.DECORATE_PAGE_RENDER_LINK, 0, String.format(
179                "ActivationRequestParameterWorker decorate page render link event handler for field %s as query parameter '%s'", fieldName, parameterName), handler);
180    }
181
182    private String getParameterName(PlasticField field, ActivationRequestParameter annotation)
183    {
184        if (annotation.value().equals(""))
185            return field.getName();
186
187        return annotation.value();
188    }
189
190}