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