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