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