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