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