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