001/** 002 * 003 * Copyright 2020-2024 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 */ 017package org.jivesoftware.smackx.xdata.form; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.jivesoftware.smackx.xdata.AbstractMultiFormField; 030import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField; 031import org.jivesoftware.smackx.xdata.FormField; 032import org.jivesoftware.smackx.xdata.FormFieldChildElement; 033import org.jivesoftware.smackx.xdata.ListMultiFormField; 034import org.jivesoftware.smackx.xdata.ListSingleFormField; 035import org.jivesoftware.smackx.xdata.packet.DataForm; 036 037import org.jxmpp.jid.Jid; 038import org.jxmpp.jid.impl.JidCreate; 039import org.jxmpp.jid.util.JidUtil; 040import org.jxmpp.stringprep.XmppStringprepException; 041import org.jxmpp.util.XmppDateTime; 042 043public class FillableForm extends FilledForm { 044 045 private final Set<String> requiredFields; 046 047 private final Set<String> filledRequiredFields = new HashSet<>(); 048 private final Set<String> missingRequiredFields = new HashSet<>(); 049 050 private final Map<String, FormField> filledFields = new HashMap<>(); 051 052 public FillableForm(DataForm dataForm) { 053 super(dataForm); 054 if (dataForm.getType() != DataForm.Type.form) { 055 throw new IllegalArgumentException(); 056 } 057 058 Set<String> requiredFields = new HashSet<>(); 059 List<FormField> requiredFieldsWithDefaultValue = new ArrayList<>(); 060 for (FormField formField : dataForm.getFields()) { 061 if (formField.isRequired()) { 062 String fieldName = formField.getFieldName(); 063 requiredFields.add(fieldName); 064 065 if (formField.hasValueSet()) { 066 // This is a form field with a default value. 067 requiredFieldsWithDefaultValue.add(formField); 068 } else { 069 missingRequiredFields.add(fieldName); 070 } 071 } 072 } 073 this.requiredFields = Collections.unmodifiableSet(requiredFields); 074 075 for (FormField field : requiredFieldsWithDefaultValue) { 076 write(field); 077 } 078 } 079 080 protected void writeListMulti(String fieldName, List<? extends CharSequence> values) { 081 FormField formField = FormField.listMultiBuilder(fieldName) 082 .addValues(values) 083 .build(); 084 write(formField); 085 } 086 087 protected void writeTextSingle(String fieldName, CharSequence value) { 088 FormField formField = FormField.textSingleBuilder(fieldName) 089 .setValue(value) 090 .build(); 091 write(formField); 092 } 093 094 protected void writeBoolean(String fieldName, boolean value) { 095 FormField formField = FormField.booleanBuilder(fieldName) 096 .setValue(value) 097 .build(); 098 write(formField); 099 } 100 101 protected void write(String fieldName, int value) { 102 writeTextSingle(fieldName, Integer.toString(value)); 103 } 104 105 protected void write(String fieldName, Date date) { 106 writeTextSingle(fieldName, XmppDateTime.formatXEP0082Date(date)); 107 } 108 109 public void setAnswer(String fieldName, Collection<? extends CharSequence> answers) { 110 FormField blankField = getFieldOrThrow(fieldName); 111 FormField.Type type = blankField.getType(); 112 113 FormField filledFormField; 114 switch (type) { 115 case list_multi: 116 case text_multi: 117 filledFormField = createMultiKindFieldbuilder(fieldName, type) 118 .addValues(answers) 119 .build(); 120 break; 121 case jid_multi: 122 List<Jid> jids = new ArrayList<>(answers.size()); 123 List<XmppStringprepException> exceptions = new ArrayList<>(); 124 JidUtil.jidsFrom(answers, jids, exceptions); 125 if (!exceptions.isEmpty()) { 126 // TODO: Report all exceptions here. 127 throw new IllegalArgumentException(exceptions.get(0)); 128 } 129 filledFormField = FormField.jidMultiBuilder(fieldName) 130 .addValues(jids) 131 .build(); 132 break; 133 default: 134 throw new IllegalArgumentException(""); 135 } 136 write(filledFormField); 137 } 138 139 private static AbstractMultiFormField.Builder<?, ?> createMultiKindFieldbuilder(String fieldName, FormField.Type type) { 140 switch (type) { 141 case list_multi: 142 return FormField.listMultiBuilder(fieldName); 143 case text_multi: 144 return FormField.textMultiBuilder(fieldName); 145 default: 146 throw new IllegalArgumentException(); 147 } 148 } 149 150 public void setAnswer(String fieldName, int answer) { 151 setAnswer(fieldName, Integer.toString(answer)); 152 } 153 154 public void setAnswer(String fieldName, CharSequence answer) { 155 FormField blankField = getFieldOrThrow(fieldName); 156 FormField.Type type = blankField.getType(); 157 158 FormField filledFormField; 159 switch (type) { 160 case list_multi: 161 case jid_multi: 162 throw new IllegalArgumentException("Can not answer fields of type '" + type + "' with a CharSequence"); 163 case fixed: 164 throw new IllegalArgumentException("Fields of type 'fixed' are not answerable"); 165 case list_single: 166 case text_private: 167 case text_single: 168 case hidden: 169 filledFormField = createSingleKindFieldBuilder(fieldName, type) 170 .setValue(answer) 171 .build(); 172 break; 173 case bool: 174 filledFormField = FormField.booleanBuilder(fieldName) 175 .setValue(answer) 176 .build(); 177 break; 178 case jid_single: 179 Jid jid; 180 try { 181 jid = JidCreate.from(answer); 182 } catch (XmppStringprepException e) { 183 throw new IllegalArgumentException(e); 184 } 185 filledFormField = FormField.jidSingleBuilder(fieldName) 186 .setValue(jid) 187 .build(); 188 break; 189 case text_multi: 190 filledFormField = createMultiKindFieldbuilder(fieldName, type) 191 .addValue(answer) 192 .build(); 193 break; 194 default: 195 throw new AssertionError(); 196 } 197 write(filledFormField); 198 } 199 200 private static AbstractSingleStringValueFormField.Builder<?, ?> createSingleKindFieldBuilder(String fieldName, FormField.Type type) { 201 switch (type) { 202 case text_private: 203 return FormField.textPrivateBuilder(fieldName); 204 case text_single: 205 return FormField.textSingleBuilder(fieldName); 206 case hidden: 207 return FormField.hiddenBuilder(fieldName); 208 case list_single: 209 return FormField.listSingleBuilder(fieldName); 210 default: 211 throw new IllegalArgumentException("Unsupported type: " + type); 212 } 213 } 214 215 public void setAnswer(String fieldName, boolean answer) { 216 FormField blankField = getFieldOrThrow(fieldName); 217 if (blankField.getType() != FormField.Type.bool) { 218 throw new IllegalArgumentException(); 219 } 220 221 FormField filledFormField = FormField.booleanBuilder(fieldName) 222 .setValue(answer) 223 .build(); 224 write(filledFormField); 225 } 226 227 public final void write(FormField filledFormField) { 228 if (filledFormField.getType() == FormField.Type.fixed) { 229 throw new IllegalArgumentException(); 230 } 231 232 String fieldName = filledFormField.getFieldName(); 233 234 boolean isListField = filledFormField instanceof ListMultiFormField 235 || filledFormField instanceof ListSingleFormField; 236 // Only list-* fields require a value to be set. Other fields types can be empty. For example MUC's 237 // muc#roomconfig_roomadmins, which is of type jid-multi, is submitted without values to reset the room's admin 238 // list. 239 if (isListField && !filledFormField.hasValueSet()) { 240 throw new IllegalArgumentException("Tried to write form field " + fieldName + " of type " 241 + filledFormField.getType() 242 + " without any values set. However, according to XEP-0045 ยง 3.3 fields of type list-multi or list-single must have one item set."); 243 } 244 245 if (!getDataForm().hasField(fieldName)) { 246 throw new IllegalArgumentException(); 247 } 248 249 // Perform validation, e.g. using XEP-0122. 250 // TODO: We could also perform list-* option validation, but this has to take xep122's <open/> into account. 251 FormField formFieldPrototype = getDataForm().getField(fieldName); 252 for (FormFieldChildElement formFieldChildelement : formFieldPrototype.getFormFieldChildElements()) { 253 formFieldChildelement.validate(filledFormField); 254 } 255 256 filledFields.put(fieldName, filledFormField); 257 if (requiredFields.contains(fieldName)) { 258 filledRequiredFields.add(fieldName); 259 missingRequiredFields.remove(fieldName); 260 } 261 } 262 263 @Override 264 public FormField getField(String fieldName) { 265 FormField filledField = filledFields.get(fieldName); 266 if (filledField != null) { 267 return filledField; 268 } 269 270 return super.getField(fieldName); 271 } 272 273 public DataForm getDataFormToSubmit() { 274 if (!missingRequiredFields.isEmpty()) { 275 throw new IllegalStateException("Not all required fields filled. Missing: " + missingRequiredFields); 276 } 277 DataForm.Builder builder = DataForm.builder(); 278 279 // the submit form has the same FORM_TYPE as the form. 280 if (formTypeFormField != null) { 281 builder.addField(formTypeFormField); 282 } 283 284 builder.addFields(filledFields.values()); 285 286 return builder.build(); 287 } 288 289 public SubmitForm getSubmitForm() { 290 DataForm form = getDataFormToSubmit(); 291 return new SubmitForm(form); 292 } 293}