001/** 002 * 003 * Copyright 2003-2007 Jive Software 2020-2022 Florian Schmaus. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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 */ 017 018package org.jivesoftware.smackx.xdata.provider; 019 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.List; 024import java.util.Set; 025import java.util.concurrent.CopyOnWriteArraySet; 026import java.util.logging.Logger; 027 028import javax.xml.namespace.QName; 029 030import org.jivesoftware.smack.packet.XmlEnvironment; 031import org.jivesoftware.smack.parsing.SmackParsingException; 032import org.jivesoftware.smack.provider.ExtensionElementProvider; 033import org.jivesoftware.smack.roster.packet.RosterPacket; 034import org.jivesoftware.smack.roster.provider.RosterPacketProvider; 035import org.jivesoftware.smack.util.EqualsUtil; 036import org.jivesoftware.smack.util.HashCode; 037import org.jivesoftware.smack.xml.XmlPullParser; 038import org.jivesoftware.smack.xml.XmlPullParserException; 039 040import org.jivesoftware.smackx.formtypes.FormFieldRegistry; 041import org.jivesoftware.smackx.xdata.AbstractMultiFormField; 042import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField; 043import org.jivesoftware.smackx.xdata.BooleanFormField; 044import org.jivesoftware.smackx.xdata.FormField; 045import org.jivesoftware.smackx.xdata.FormFieldChildElement; 046import org.jivesoftware.smackx.xdata.FormFieldWithOptions; 047import org.jivesoftware.smackx.xdata.JidMultiFormField; 048import org.jivesoftware.smackx.xdata.JidSingleFormField; 049import org.jivesoftware.smackx.xdata.ListMultiFormField; 050import org.jivesoftware.smackx.xdata.ListSingleFormField; 051import org.jivesoftware.smackx.xdata.TextSingleFormField; 052import org.jivesoftware.smackx.xdata.packet.DataForm; 053import org.jivesoftware.smackx.xdatalayout.packet.DataLayout; 054import org.jivesoftware.smackx.xdatalayout.provider.DataLayoutProvider; 055 056/** 057 * The DataFormProvider parses DataForm packets. 058 * 059 * @author Gaston Dombiak 060 */ 061public class DataFormProvider extends ExtensionElementProvider<DataForm> { 062 063 private static final Logger LOGGER = Logger.getLogger(DataFormProvider.class.getName()); 064 065 public static final DataFormProvider INSTANCE = new DataFormProvider(); 066 067 @Override 068 public DataForm parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { 069 DataForm.Type dataFormType = DataForm.Type.fromString(parser.getAttributeValue("", "type")); 070 DataForm.Builder dataForm = DataForm.builder(dataFormType); 071 072 String formType = null; 073 DataForm.ReportedData reportedData = null; 074 075 outerloop: while (true) { 076 XmlPullParser.Event eventType = parser.next(); 077 switch (eventType) { 078 case START_ELEMENT: 079 String name = parser.getName(); 080 String namespace = parser.getNamespace(); 081 XmlEnvironment elementXmlEnvironment = XmlEnvironment.from(parser, xmlEnvironment); 082 switch (name) { 083 case "instructions": 084 dataForm.addInstruction(parser.nextText()); 085 break; 086 case "title": 087 dataForm.setTitle(parser.nextText()); 088 break; 089 case "field": 090 // Note that we parse this form field without any potential reportedData. We only use reportedData 091 // to lookup form field types of fields under <item/>. 092 FormField formField = parseField(parser, elementXmlEnvironment, formType); 093 094 TextSingleFormField hiddenFormTypeField = formField.asHiddenFormTypeFieldIfPossible(); 095 if (hiddenFormTypeField != null) { 096 if (formType != null) { 097 throw new SmackParsingException("Multiple hidden form type fields"); 098 } 099 formType = hiddenFormTypeField.getValue(); 100 } 101 102 dataForm.addField(formField); 103 break; 104 case "item": 105 DataForm.Item item = parseItem(parser, elementXmlEnvironment, formType, reportedData); 106 dataForm.addItem(item); 107 break; 108 case "reported": 109 if (reportedData != null) { 110 throw new SmackParsingException("Data form with multiple <reported/> elements"); 111 } 112 reportedData = parseReported(parser, elementXmlEnvironment, formType); 113 dataForm.setReportedData(reportedData); 114 break; 115 // See XEP-133 Example 32 for a corner case where the data form contains this extension. 116 case RosterPacket.ELEMENT: 117 if (namespace.equals(RosterPacket.NAMESPACE)) { 118 dataForm.addExtensionElement(RosterPacketProvider.INSTANCE.parse(parser)); 119 } 120 break; 121 // See XEP-141 Data Forms Layout 122 case DataLayout.ELEMENT: 123 if (namespace.equals(DataLayout.NAMESPACE)) { 124 dataForm.addExtensionElement(DataLayoutProvider.parse(parser)); 125 } 126 break; 127 } 128 break; 129 case END_ELEMENT: 130 if (parser.getDepth() == initialDepth) { 131 break outerloop; 132 } 133 break; 134 default: 135 // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement. 136 break; 137 } 138 } 139 return dataForm.build(); 140 } 141 142 private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType) 143 throws XmlPullParserException, IOException, SmackParsingException { 144 return parseField(parser, xmlEnvironment, formType, null); 145 } 146 147 private static final class FieldNameAndFormType { 148 private final String fieldName; 149 private final String formType; 150 151 private FieldNameAndFormType(String fieldName, String formType) { 152 this.fieldName = fieldName; 153 this.formType = formType; 154 } 155 156 private final HashCode.Cache hashCodeCache = new HashCode.Cache(); 157 158 @Override 159 public int hashCode() { 160 return hashCodeCache.getHashCode(b -> 161 b.append(fieldName) 162 .append(formType) 163 .build() 164 ); 165 } 166 167 @Override 168 public boolean equals(Object other) { 169 return EqualsUtil.equals(this, other, (e, o) -> 170 e.append(fieldName, o.fieldName) 171 .append(formType, o.formType) 172 ); 173 } 174 } 175 176 private static final Set<FieldNameAndFormType> UNKNOWN_FIELDS = new CopyOnWriteArraySet<>(); 177 178 private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType, DataForm.ReportedData reportedData) 179 throws XmlPullParserException, IOException, SmackParsingException { 180 final int initialDepth = parser.getDepth(); 181 182 final String fieldName = parser.getAttributeValue("var"); 183 final String label = parser.getAttributeValue("", "label"); 184 185 FormField.Type type = null; 186 { 187 String fieldTypeString = parser.getAttributeValue("type"); 188 if (fieldTypeString != null) { 189 type = FormField.Type.fromString(fieldTypeString); 190 } 191 } 192 193 List<FormField.Value> values = new ArrayList<>(); 194 List<FormField.Option> options = new ArrayList<>(); 195 List<FormFieldChildElement> childElements = new ArrayList<>(); 196 boolean required = false; 197 198 outerloop: while (true) { 199 XmlPullParser.TagEvent eventType = parser.nextTag(); 200 switch (eventType) { 201 case START_ELEMENT: 202 QName qname = parser.getQName(); 203 if (qname.equals(FormField.Value.QNAME)) { 204 FormField.Value value = parseValue(parser); 205 values.add(value); 206 } else if (qname.equals(FormField.Option.QNAME)) { 207 FormField.Option option = parseOption(parser); 208 options.add(option); 209 } else if (qname.equals(FormField.Required.QNAME)) { 210 required = true; 211 } else { 212 FormFieldChildElementProvider<?> provider = FormFieldChildElementProviderManager.getFormFieldChildElementProvider( 213 qname); 214 if (provider == null) { 215 LOGGER.warning("Unknown form field child element " + qname + " ignored"); 216 continue; 217 } 218 FormFieldChildElement formFieldChildElement = provider.parse(parser, 219 XmlEnvironment.from(parser, xmlEnvironment)); 220 childElements.add(formFieldChildElement); 221 } 222 break; 223 case END_ELEMENT: 224 if (parser.getDepth() == initialDepth) { 225 break outerloop; 226 } 227 break; 228 } 229 } 230 231 // Data forms of type 'result' may contain <reported/> and <item/> elements. If this is the case, then the type 232 // of the <field/>s within the <item/> elements is determined by the information found in <reported/>. See 233 // XEP-0004 ยง 3.4 and SMACK-902 234 if (type == null && reportedData != null) { 235 FormField reportedFormField = reportedData.getField(fieldName); 236 if (reportedFormField != null) { 237 type = reportedFormField.getType(); 238 } 239 } 240 241 if (type == null) { 242 // The field name 'FORM_TYPE' is magic. 243 if (fieldName.equals(FormField.FORM_TYPE)) { 244 type = FormField.Type.hidden; 245 } else { 246 // If no field type was explicitly provided, then we need to lookup the 247 // field's type in the registry. 248 type = FormFieldRegistry.lookup(formType, fieldName); 249 if (type == null) { 250 FieldNameAndFormType fieldNameAndFormType = new FieldNameAndFormType(fieldName, formType); 251 if (!UNKNOWN_FIELDS.contains(fieldNameAndFormType)) { 252 LOGGER.warning("The Field '" + fieldName + "' from FORM_TYPE '" + formType 253 + "' is not registered. Field type is unknown, assuming text-single."); 254 UNKNOWN_FIELDS.add(fieldNameAndFormType); 255 } 256 // As per XEP-0004, text-single is the default form field type, which we use as emergency fallback here. 257 type = FormField.Type.text_single; 258 } 259 } 260 } 261 262 FormField.Builder<?, ?> builder; 263 switch (type) { 264 case bool: 265 builder = parseBooleanFormField(fieldName, values); 266 break; 267 case fixed: 268 builder = parseSingleKindFormField(FormField.fixedBuilder(fieldName), values); 269 break; 270 case hidden: 271 builder = parseSingleKindFormField(FormField.hiddenBuilder(fieldName), values); 272 break; 273 case jid_multi: 274 JidMultiFormField.Builder jidMultiBuilder = FormField.jidMultiBuilder(fieldName); 275 for (FormField.Value value : values) { 276 jidMultiBuilder.addValue(value); 277 } 278 builder = jidMultiBuilder; 279 break; 280 case jid_single: 281 ensureAtMostSingleValue(type, values); 282 JidSingleFormField.Builder jidSingleBuilder = FormField.jidSingleBuilder(fieldName); 283 if (!values.isEmpty()) { 284 FormField.Value value = values.get(0); 285 jidSingleBuilder.setValue(value); 286 } 287 builder = jidSingleBuilder; 288 break; 289 case list_multi: 290 ListMultiFormField.Builder listMultiBuilder = FormField.listMultiBuilder(fieldName); 291 addOptionsToBuilder(options, listMultiBuilder); 292 builder = parseMultiKindFormField(listMultiBuilder, values); 293 break; 294 case list_single: 295 ListSingleFormField.Builder listSingleBuilder = FormField.listSingleBuilder(fieldName); 296 addOptionsToBuilder(options, listSingleBuilder); 297 builder = parseSingleKindFormField(listSingleBuilder, values); 298 break; 299 case text_multi: 300 builder = parseMultiKindFormField(FormField.textMultiBuilder(fieldName), values); 301 break; 302 case text_private: 303 builder = parseSingleKindFormField(FormField.textPrivateBuilder(fieldName), values); 304 break; 305 case text_single: 306 builder = parseSingleKindFormField(FormField.textSingleBuilder(fieldName), values); 307 break; 308 default: 309 // Should never happen, as we cover all types in the switch/case. 310 throw new AssertionError("Unknown type " + type); 311 } 312 313 314 switch (type) { 315 case list_multi: 316 case list_single: 317 break; 318 default: 319 if (!options.isEmpty()) { 320 throw new SmackParsingException("Form fields of type " + type + " must not have options. This one had " 321 + options.size()); 322 } 323 break; 324 } 325 326 if (label != null) { 327 builder.setLabel(label); 328 } 329 builder.setRequired(required); 330 builder.addFormFieldChildElements(childElements); 331 332 return builder.build(); 333 } 334 335 private static FormField.Builder<?, ?> parseBooleanFormField(String fieldName, List<FormField.Value> values) throws SmackParsingException { 336 BooleanFormField.Builder builder = FormField.booleanBuilder(fieldName); 337 ensureAtMostSingleValue(builder.getType(), values); 338 if (values.size() == 1) { 339 FormField.Value value = values.get(0); 340 builder.setValue(value); 341 } 342 return builder; 343 } 344 345 private static AbstractSingleStringValueFormField.Builder<?, ?> parseSingleKindFormField( 346 AbstractSingleStringValueFormField.Builder<?, ?> builder, List<FormField.Value> values) 347 throws SmackParsingException { 348 ensureAtMostSingleValue(builder.getType(), values); 349 if (values.size() == 1) { 350 String value = values.get(0).getValue().toString(); 351 builder.setValue(value); 352 } 353 return builder; 354 } 355 356 private static AbstractMultiFormField.Builder<?, ?> parseMultiKindFormField(AbstractMultiFormField.Builder<?, ?> builder, 357 List<FormField.Value> values) { 358 for (FormField.Value value : values) { 359 builder.addValue(value.getValue()); 360 } 361 return builder; 362 } 363 364 private static DataForm.Item parseItem(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType, 365 DataForm.ReportedData reportedData) 366 throws XmlPullParserException, IOException, SmackParsingException { 367 final int initialDepth = parser.getDepth(); 368 List<FormField> fields = new ArrayList<>(); 369 outerloop: while (true) { 370 XmlPullParser.TagEvent eventType = parser.nextTag(); 371 switch (eventType) { 372 case START_ELEMENT: 373 String name = parser.getName(); 374 switch (name) { 375 case "field": 376 FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), formType, 377 reportedData); 378 fields.add(field); 379 break; 380 } 381 break; 382 case END_ELEMENT: 383 if (parser.getDepth() == initialDepth) { 384 break outerloop; 385 } 386 break; 387 } 388 } 389 return new DataForm.Item(fields); 390 } 391 392 private static DataForm.ReportedData parseReported(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType) 393 throws XmlPullParserException, IOException, SmackParsingException { 394 final int initialDepth = parser.getDepth(); 395 List<FormField> fields = new ArrayList<>(); 396 outerloop: while (true) { 397 XmlPullParser.TagEvent eventType = parser.nextTag(); 398 switch (eventType) { 399 case START_ELEMENT: 400 String name = parser.getName(); 401 switch (name) { 402 case "field": 403 FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), formType); 404 fields.add(field); 405 break; 406 } 407 break; 408 case END_ELEMENT: 409 if (parser.getDepth() == initialDepth) { 410 break outerloop; 411 } 412 break; 413 } 414 } 415 return new DataForm.ReportedData(fields); 416 } 417 418 public static FormField.Value parseValue(XmlPullParser parser) throws IOException, XmlPullParserException { 419 String value = parser.nextText(); 420 return new FormField.Value(value); 421 } 422 423 public static FormField.Option parseOption(XmlPullParser parser) throws IOException, XmlPullParserException { 424 int initialDepth = parser.getDepth(); 425 FormField.Option option = null; 426 String label = parser.getAttributeValue("", "label"); 427 outerloop: while (true) { 428 XmlPullParser.TagEvent eventType = parser.nextTag(); 429 switch (eventType) { 430 case START_ELEMENT: 431 String name = parser.getName(); 432 switch (name) { 433 case "value": 434 option = new FormField.Option(label, parser.nextText()); 435 break; 436 } 437 break; 438 case END_ELEMENT: 439 if (parser.getDepth() == initialDepth) { 440 break outerloop; 441 } 442 break; 443 } 444 } 445 return option; 446 } 447 448 private static void ensureAtMostSingleValue(FormField.Type type, List<FormField.Value> values) throws SmackParsingException { 449 if (values.size() > 1) { 450 throw new SmackParsingException(type + " fields can have at most one value, this one had " + values.size()); 451 } 452 } 453 454 private static void addOptionsToBuilder(Collection<FormField.Option> options, FormFieldWithOptions.Builder<?> builder) { 455 for (FormField.Option option : options) { 456 builder.addOption(option); 457 } 458 } 459}