View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.renderkit.renderer;
21  
22  import org.apache.myfaces.tobago.context.TobagoContext;
23  import org.apache.myfaces.tobago.internal.component.AbstractUIFile;
24  import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
25  import org.apache.myfaces.tobago.internal.util.HttpPartWrapper;
26  import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
27  import org.apache.myfaces.tobago.renderkit.css.Icons;
28  import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
29  import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
30  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
31  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
32  import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
33  import org.apache.myfaces.tobago.util.ComponentUtils;
34  import org.apache.myfaces.tobago.util.ResourceUtils;
35  import org.apache.myfaces.tobago.validator.FileItemValidator;
36  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import javax.faces.context.FacesContext;
41  import javax.faces.event.ComponentSystemEvent;
42  import javax.faces.event.ComponentSystemEventListener;
43  import javax.faces.event.ListenerFor;
44  import javax.faces.event.PostAddToViewEvent;
45  import javax.faces.validator.Validator;
46  import javax.servlet.http.HttpServletRequest;
47  import javax.servlet.http.Part;
48  import java.io.IOException;
49  import java.lang.invoke.MethodHandles;
50  import java.util.ArrayList;
51  import java.util.List;
52  
53  @ListenerFor(systemEventClass = PostAddToViewEvent.class)
54  public class FileRenderer<T extends AbstractUIFile>
55      extends MessageLayoutRendererBase<T> implements ComponentSystemEventListener {
56  
57    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
58  
59    @Override
60    protected boolean isOutputOnly(T component) {
61      return component.isDisabled() || component.isReadonly();
62    }
63  
64    @Override
65    public HtmlElements getComponentTag() {
66      return HtmlElements.TOBAGO_FILE;
67    }
68  
69    @Override
70    public void processEvent(final ComponentSystemEvent event) {
71      TobagoContext.getInstance(FacesContext.getCurrentInstance()).setEnctype("multipart/form-data");
72    }
73  
74    @Override
75    public boolean getRendersChildren() {
76      return true;
77    }
78  
79    @Override
80    public void decodeInternal(final FacesContext facesContext, final T component) {
81      if (isOutputOnly(component)) {
82        return;
83      }
84  
85      final boolean multiple = component.isMultiple() && !component.isRequired();
86      final Object request = facesContext.getExternalContext().getRequest();
87      if (request instanceof HttpServletRequest) {
88        try {
89          final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
90          if (multiple) {
91            final List<Part> parts = new ArrayList<>();
92            for (final Part part : httpServletRequest.getParts()) {
93              if (component.getClientId(facesContext).equals(part.getName())) {
94                LOG.debug("Uploaded file '{}', size={}, type='{}'",
95                    part.getSubmittedFileName(), part.getSize(), part.getContentType());
96                parts.add(new HttpPartWrapper(part));
97              }
98              component.setSubmittedValue(parts.toArray(new Part[0]));
99            }
100         } else {
101           final Part part = httpServletRequest.getPart(component.getClientId(facesContext));
102           final String submittedFileName = part.getSubmittedFileName();
103           if (LOG.isDebugEnabled()) {
104             LOG.debug("Uploaded file '{}', size={}, type='{}'",
105                 submittedFileName, part.getSize(), part.getContentType());
106           }
107           if (submittedFileName.length() > 0) {
108             component.setSubmittedValue(new HttpPartWrapper(part));
109           }
110         }
111       } catch (final Exception e) {
112         LOG.error("", e);
113         component.setValid(false);
114       }
115     } else { // todo: PortletRequest
116       LOG.warn("Unsupported request type: " + request.getClass().getName());
117     }
118 
119     decodeClientBehaviors(facesContext, component);
120   }
121 
122   @Override
123   protected void encodeAttributes(final FacesContext facesContext, final T component) throws IOException {
124     final String placeholder = component.getPlaceholder();
125     final String multiFormat = ResourceUtils.getString(facesContext, "file.selected");
126 
127     final TobagoResponseWriter writer = getResponseWriter(facesContext);
128     writer.writeAttribute(HtmlAttributes.PLACEHOLDER, placeholder, true);
129     writer.writeAttribute(CustomAttributes.MULTI_FORMAT, multiFormat, true);
130   }
131 
132   @Override
133   protected void encodeBeginField(final FacesContext facesContext, final T component) throws IOException {
134 
135     final String clientId = component.getClientId(facesContext);
136     final String fieldId = component.getFieldId(facesContext);
137     final String accept = createAcceptFromValidators(component);
138     final boolean multiple = component.isMultiple() && !component.isRequired();
139     final boolean disabled = component.isDisabled();
140     final boolean readonly = component.isReadonly();
141     if (component.isMultiple() && component.isRequired()) {
142       LOG.warn("Required multiple file upload is not supported."); //TODO TOBAGO-1930
143     }
144 
145     final TobagoResponseWriter writer = getResponseWriter(facesContext);
146 
147     writer.startElement(HtmlElements.DIV);
148     writer.writeClassAttribute(
149         BootstrapClass.FORM_FILE,
150         TobagoClass.FILE.createMarkup(component.getMarkup()),
151         component.getCustomClass(),
152         BootstrapClass.FORM_CONTROL_PLAINTEXT);
153     HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
154 
155     writer.startElement(HtmlElements.INPUT);
156     writer.writeAttribute(HtmlAttributes.MULTIPLE, multiple);
157     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.FILE);
158     writer.writeAttribute(HtmlAttributes.ACCEPT, accept, true);
159     writer.writeAttribute(HtmlAttributes.TABINDEX, -1);
160     writer.writeIdAttribute(fieldId);
161     writer.writeClassAttribute(
162         BootstrapClass.FORM_FILE_INPUT,
163         BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)));
164     writer.writeNameAttribute(clientId);
165     // readonly seems not making sense in browsers.
166     writer.writeAttribute(HtmlAttributes.DISABLED, disabled || readonly);
167     writer.writeAttribute(HtmlAttributes.READONLY, readonly);
168     writer.writeAttribute(HtmlAttributes.REQUIRED, component.isRequired());
169     // TODO Focus
170     //HtmlRendererUtils.renderFocus(clientId, file.isFocus(), ComponentUtils.isError(file), facesContext, writer);
171     final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, component);
172     if (title != null) {
173       writer.writeAttribute(HtmlAttributes.TITLE, title, true);
174     }
175     writer.endElement(HtmlElements.INPUT);
176 
177     encodeBehavior(writer, facesContext, component);
178 
179     writer.startElement(HtmlElements.LABEL);
180     writer.writeClassAttribute(BootstrapClass.FORM_FILE_LABEL);
181     writer.writeAttribute(HtmlAttributes.FOR, fieldId, false);
182     writer.startElement(HtmlElements.SPAN);
183     writer.writeClassAttribute(BootstrapClass.FORM_FILE_TEXT);
184     writer.endElement(HtmlElements.SPAN);
185     writer.startElement(HtmlElements.SPAN);
186     writer.writeClassAttribute(BootstrapClass.FORM_FILE_BUTTON);
187     writer.startElement(HtmlElements.I);
188     // TODO: define a name
189     writer.writeAttribute(HtmlAttributes.TITLE, "Browse", false);
190     writer.writeClassAttribute(Icons.FA, Icons.FOLDER_OPEN);
191     writer.endElement(HtmlElements.I);
192     writer.endElement(HtmlElements.SPAN);
193     writer.endElement(HtmlElements.LABEL);
194   }
195 
196   private String createAcceptFromValidators(final AbstractUIFile file) {
197     final StringBuilder builder = new StringBuilder();
198     for (final Validator validator : file.getValidators()) {
199       if (validator instanceof FileItemValidator) {
200         final FileItemValidator fileItemValidator = (FileItemValidator) validator;
201         for (final String contentType : fileItemValidator.getContentType()) {
202           builder.append(",");
203           builder.append(contentType);
204         }
205       }
206     }
207     if (builder.length() > 0) {
208       return builder.substring(1);
209     } else {
210       return null;
211     }
212   }
213 
214   @Override
215   protected void encodeEndField(final FacesContext facesContext, final T component) throws IOException {
216     final TobagoResponseWriter writer = getResponseWriter(facesContext);
217     writer.endElement(HtmlElements.DIV);
218   }
219 
220   @Override
221   protected String getFieldId(final FacesContext facesContext, final T component) {
222     return component.getFieldId(facesContext);
223   }
224 }