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