View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.component.inputphone;
23  
24  import java.io.IOException;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Map;
28  import java.util.logging.Logger;
29  
30  import javax.faces.FacesException;
31  import javax.faces.component.UIComponent;
32  import javax.faces.context.FacesContext;
33  import javax.faces.context.ResponseWriter;
34  import javax.faces.convert.Converter;
35  
36  import org.primefaces.component.inputtext.InputText;
37  import org.primefaces.extensions.config.PrimeExtensionsEnvironment;
38  import org.primefaces.extensions.util.Attrs;
39  import org.primefaces.extensions.util.PhoneNumberUtilWrapper;
40  import org.primefaces.renderkit.InputRenderer;
41  import org.primefaces.shaded.json.JSONArray;
42  import org.primefaces.shaded.json.JSONObject;
43  import org.primefaces.util.ComponentUtils;
44  import org.primefaces.util.Constants;
45  import org.primefaces.util.HTML;
46  import org.primefaces.util.LangUtils;
47  import org.primefaces.util.WidgetBuilder;
48  
49  /**
50   * Renderer for the {@link InputPhone} component.
51   *
52   * @author Jasper de Vries <jepsar@gmail.com>
53   * @since 7.0
54   */
55  public class InputPhoneRenderer extends InputRenderer {
56  
57      private static final Logger LOGGER = Logger.getLogger(InputPhoneRenderer.class.getName());
58      private static final String HIDDEN_ID = "_hidden";
59      private static final String ISO2_ID = "_iso2";
60  
61      @Override
62      public void decode(final FacesContext context, final UIComponent component) {
63          final InputPhone inputPhone = (InputPhone) component;
64  
65          if (!shouldDecode(inputPhone)) {
66              return;
67          }
68  
69          decodeBehaviors(context, inputPhone);
70  
71          final String inputId = inputPhone.getClientId(context) + HIDDEN_ID;
72          final String submittedValue = context.getExternalContext().getRequestParameterMap().get(inputId);
73  
74          if (submittedValue != null) {
75              inputPhone.setSubmittedValue(submittedValue);
76          }
77      }
78  
79      @Override
80      public void encodeEnd(final FacesContext context, final UIComponent component) throws IOException {
81          final InputPhone inputPhone = (InputPhone) component;
82  
83          final Object value = inputPhone.getValue();
84          String valueToRender = ComponentUtils.getValueToRender(context, inputPhone, value);
85          if (valueToRender == null) {
86              valueToRender = Constants.EMPTY_STRING;
87          }
88  
89          encodeMarkup(context, inputPhone, valueToRender);
90          encodeScript(context, inputPhone);
91      }
92  
93      @Override
94      public Object getConvertedValue(final FacesContext context, final UIComponent component,
95                  final Object submittedValue) {
96          final String value = (String) submittedValue;
97          if (LangUtils.isBlank(value)) {
98              return null;
99          }
100 
101         final InputPhone inputPhone = (InputPhone) component;
102         final Converter<?> converter = inputPhone.getConverter();
103 
104         if (converter != null) {
105             return converter.getAsObject(context, inputPhone, value);
106         }
107 
108         String country = context.getExternalContext().getRequestParameterMap()
109                     .get(inputPhone.getClientId() + ISO2_ID);
110         if (country == null || InputPhone.COUNTRY_AUTO.equals(country)) {
111             country = Constants.EMPTY_STRING;
112         }
113         else {
114             inputPhone.setInitialCountry(country);
115         }
116         if (PrimeExtensionsEnvironment.getCurrentInstance(context).isLibphonenumberAvailable()) {
117             PhoneNumberUtilWrapper.validate(value, country.toUpperCase(), inputPhone.getValidatorMessage());
118         }
119         else {
120             LOGGER.warning("Libphonenumber not available, unable to validate!");
121         }
122         return value;
123     }
124 
125     protected void encodeMarkup(final FacesContext context, final InputPhone inputPhone, final String valueToRender)
126                 throws IOException {
127         final ResponseWriter writer = context.getResponseWriter();
128         final String clientId = inputPhone.getClientId(context);
129         final String styleClass = getStyleClassBuilder(context)
130                     .add(InputPhone.STYLE_CLASS)
131                     .add(inputPhone.getStyleClass())
132                     .build();
133 
134         writer.startElement("span", inputPhone);
135         writer.writeAttribute("id", clientId, null);
136         writer.writeAttribute(Attrs.CLASS, styleClass, "styleClass");
137 
138         if (inputPhone.getStyle() != null) {
139             writer.writeAttribute(Attrs.STYLE, inputPhone.getStyle(), Attrs.STYLE);
140         }
141 
142         renderRTLDirection(context, inputPhone);
143         encodeInput(context, inputPhone, clientId, valueToRender);
144         encodeHiddenInputs(context, inputPhone, clientId, valueToRender);
145 
146         writer.endElement("span");
147     }
148 
149     protected void encodeInput(final FacesContext context, final InputPhone inputPhone, final String clientId,
150                 final String valueToRender)
151                 throws IOException {
152 
153         final ResponseWriter writer = context.getResponseWriter();
154         final String inputId = clientId + InputPhone.INPUT_SUFFIX;
155         final String inputStyle = inputPhone.getInputStyle();
156         final String styleClass = createStyleClass(inputPhone, "inputStyleClass", InputText.STYLE_CLASS);
157 
158         writer.startElement("input", null);
159         writer.writeAttribute("id", inputId, null);
160         writer.writeAttribute("name", inputId, null);
161         writer.writeAttribute("type", inputPhone.getType(), null);
162         writer.writeAttribute("value", valueToRender, null);
163         writer.writeAttribute(Attrs.CLASS, styleClass, "inputStyleClass");
164 
165         if (!isValueBlank(inputStyle)) {
166             writer.writeAttribute(Attrs.STYLE, inputStyle, null);
167         }
168 
169         renderAccessibilityAttributes(context, inputPhone);
170         renderPassThruAttributes(context, inputPhone, HTML.INPUT_TEXT_ATTRS_WITHOUT_EVENTS);
171         renderDomEvents(context, inputPhone, HTML.INPUT_TEXT_EVENTS);
172         renderValidationMetadata(context, inputPhone);
173 
174         writer.endElement("input");
175     }
176 
177     protected void encodeHiddenInputs(final FacesContext context, final InputPhone inputPhone, final String clientId, final String valueToRender)
178                 throws IOException {
179         renderHiddenInput(context, clientId + ISO2_ID, inputPhone.getInitialCountry(), inputPhone.isDisabled());
180         renderHiddenInput(context, clientId + HIDDEN_ID, valueToRender, inputPhone.isDisabled());
181     }
182 
183     protected void encodeScript(final FacesContext context, final InputPhone inputPhone) throws IOException {
184         final WidgetBuilder wb = getWidgetBuilder(context);
185         wb.init("ExtInputPhone", inputPhone);
186         encodeCountries(wb, "excludeCountries", inputPhone.getExcludeCountries());
187         wb.attr("allowDropdown", inputPhone.isAllowDropdown(), true);
188         wb.attr("autoHideDialCode", inputPhone.isAutoHideDialCode(), true);
189         wb.attr("fixDropdownWidth", inputPhone.isFixDropdownWidth(), true);
190         wb.attr("formatOnDisplay", inputPhone.isFormatOnDisplay(), true);
191         wb.attr("formatAsYouType", inputPhone.isFormatAsYouType(), true);
192         wb.attr("nationalMode", inputPhone.isNationalMode(), true);
193         wb.attr("separateDialCode", inputPhone.isSeparateDialCode(), false);
194         if (inputPhone.getAutoPlaceholderEnum() != InputPhone.AutoPlaceholder.polite) {
195             wb.attr("autoPlaceholder", inputPhone.getAutoPlaceholder());
196         }
197         if (LangUtils.isNotBlank(inputPhone.getInitialCountry())) {
198             wb.attr("initialCountry", inputPhone.getInitialCountry());
199         }
200         if (InputPhone.COUNTRY_AUTO.equals(inputPhone.getInitialCountry())) {
201             if (inputPhone.getGeoIpLookup() == null) {
202                 throw new FacesException("InputPhone geoIpLookup property is required when initialCountry is 'auto'.");
203             }
204             wb.nativeAttr("geoIpLookup", inputPhone.getGeoIpLookup());
205         }
206         encodeCountries(wb, "onlyCountries", inputPhone.getOnlyCountries());
207         if (inputPhone.getPlaceholderNumberTypeEnum() != InputPhone.PlaceholderNumberType.mobile) {
208             wb.attr("placeholderNumberType", inputPhone.getPlaceholderNumberType().toUpperCase());
209         }
210         encodeCountries(wb, "countryOrder", inputPhone.getPreferredCountries());
211 
212         wb.attr("utilsScript",
213                     context.getApplication()
214                                 .getResourceHandler()
215                                 .createResource("inputphone/utils.js", org.primefaces.extensions.util.Constants.LIBRARY)
216                                 .getRequestPath());
217         if (inputPhone.getLocalizedCountries() != null) {
218             wb.nativeAttr("i18n", objectToJsonString(inputPhone.getLocalizedCountries()));
219         }
220 
221         encodeClientBehaviors(context, inputPhone);
222 
223         wb.finish();
224     }
225 
226     private void encodeCountries(final WidgetBuilder wb, final String attribute, final Object value)
227                 throws IOException {
228         final Collection<String> countries = toCollection(value);
229         if (!countries.isEmpty()) {
230             wb.nativeAttr(attribute, new JSONArray(countries).toString());
231         }
232     }
233 
234     @SuppressWarnings("unchecked")
235     private Collection<String> toCollection(final Object object) {
236         if (object instanceof String) {
237             final String string = ((String) object).replace(' ', ',').toLowerCase();
238             return Arrays.asList(string.split(","));
239         }
240         return (Collection<String>) object;
241     }
242 
243     @SuppressWarnings("unchecked")
244     private String objectToJsonString(final Object object) {
245         if (object == null || object instanceof String) {
246             return (String) object;
247         }
248         Map<String, String> map = (Map<String, String>) object;
249         JSONObject jsonObj = new JSONObject();
250         map.forEach(jsonObj::put);
251         return jsonObj.toString();
252     }
253 
254 }