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