001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.iso;
020
021import org.jpos.iso.packager.XMLPackager;
022
023import java.io.InputStream;
024import java.io.PrintStream;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028
029/**
030 * Composite ISO field that holds one or more datasets.
031 */
032public class ISODatasetField extends ISOComponent {
033    private int fieldNumber;
034    private final List<Dataset> datasets = new ArrayList<>();
035
036    /**
037     * Creates an unbound dataset field.
038     */
039    public ISODatasetField() {
040        this(-1);
041    }
042
043    /**
044     * Creates a dataset field bound to an outer field number.
045     *
046     * @param fieldNumber outer field number
047     */
048    public ISODatasetField(int fieldNumber) {
049        this.fieldNumber = fieldNumber;
050    }
051
052    /**
053     * Appends a dataset to this field.
054     *
055     * @param dataset dataset to add
056     */
057    public void addDataset(Dataset dataset) {
058        datasets.add(dataset);
059    }
060
061    /**
062     * Removes a dataset instance from this field.
063     *
064     * @param dataset dataset to remove
065     */
066    public void removeDataset(Dataset dataset) {
067        datasets.remove(dataset);
068    }
069
070    /**
071     * Indicates whether this field still contains datasets.
072     *
073     * @return {@code true} when at least one dataset is present
074     */
075    public boolean hasDatasets() {
076        return !datasets.isEmpty();
077    }
078
079    /**
080     * Returns all datasets in insertion order.
081     *
082     * @return immutable list of datasets
083     */
084    public List<Dataset> getDatasets() {
085        return Collections.unmodifiableList(datasets);
086    }
087
088    /**
089     * Returns all datasets that match the supplied identifier.
090     *
091     * @param identifier dataset identifier
092     * @return immutable list of matching datasets
093     */
094    public List<Dataset> getDatasets(int identifier) {
095        List<Dataset> matches = new ArrayList<>();
096        for (Dataset dataset : datasets) {
097            if (dataset.getIdentifier() == identifier) {
098                matches.add(dataset);
099            }
100        }
101        return Collections.unmodifiableList(matches);
102    }
103
104    /**
105     * Returns the first dataset that matches the supplied identifier.
106     *
107     * @param identifier dataset identifier
108     * @return matching dataset or {@code null}
109     */
110    public Dataset getDataset(int identifier) {
111        for (Dataset dataset : datasets) {
112            if (dataset.getIdentifier() == identifier) {
113                return dataset;
114            }
115        }
116        return null;
117    }
118
119    /**
120     * Returns the component stored under the given dataset and element identifiers.
121     *
122     * @param datasetId dataset identifier
123     * @param elementId element identifier
124     * @return matching component or {@code null}
125     */
126    public ISOComponent get(int datasetId, int elementId) {
127        Dataset dataset = getDataset(datasetId);
128        if (dataset instanceof ISODataset) {
129            return ((ISODataset) dataset).getComponent(elementId);
130        }
131        DatasetElement element = dataset != null ? dataset.getElement(elementId) : null;
132        return element != null ? element.getComponent() : null;
133    }
134
135    /**
136     * Returns the logical value stored under the given dataset and element identifiers.
137     *
138     * @param datasetId dataset identifier
139     * @param elementId element identifier
140     * @return element value or {@code null}
141     * @throws ISOException on component access errors
142     */
143    public Object getValue(int datasetId, int elementId) throws ISOException {
144        ISOComponent component = get(datasetId, elementId);
145        return component != null ? component.getValue() : null;
146    }
147
148    /**
149     * Returns the bytes stored under the given dataset and element identifiers.
150     *
151     * @param datasetId dataset identifier
152     * @param elementId element identifier
153     * @return element bytes or {@code null}
154     * @throws ISOException on component access errors
155     */
156    public byte[] getBytes(int datasetId, int elementId) throws ISOException {
157        ISOComponent component = get(datasetId, elementId);
158        return component != null ? component.getBytes() : null;
159    }
160
161    /**
162     * Returns this composite field.
163     *
164     * @return this field
165     */
166    @Override
167    public ISOComponent getComposite() {
168        return this;
169    }
170
171    /**
172     * Returns the outer ISO field number.
173     *
174     * @return outer field number
175     */
176    @Override
177    public Object getKey() {
178        return fieldNumber;
179    }
180
181    /**
182     * Returns the datasets carried by this field.
183     *
184     * @return dataset list
185     */
186    @Override
187    public Object getValue() {
188        return getDatasets();
189    }
190
191    /**
192     * Dataset fields do not expose their bytes directly and must be packed via a
193     * {@link DatasetFieldPackager}.
194     *
195     * @return never returns normally
196     * @throws ISOException always
197     */
198    @Override
199    public byte[] getBytes() throws ISOException {
200        throw new ISOException("Dataset fields must be packed via DatasetFieldPackager");
201    }
202
203    /**
204     * Sets the outer ISO field number.
205     *
206     * @param fieldNumber outer field number
207     */
208    @Override
209    public void setFieldNumber(int fieldNumber) {
210        this.fieldNumber = fieldNumber;
211    }
212
213    /**
214     * Returns the outer ISO field number.
215     *
216     * @return outer field number
217     */
218    @Override
219    public int getFieldNumber() {
220        return fieldNumber;
221    }
222
223    /**
224     * Replaces the datasets held by this field.
225     *
226     * @param obj either a {@link Dataset} or a {@link java.util.List} of datasets
227     * @throws ISOException when the supplied value type is unsupported
228     */
229    @Override
230    public void setValue(Object obj) throws ISOException {
231        datasets.clear();
232        if (obj instanceof Dataset) {
233            datasets.add((Dataset) obj);
234        } else if (obj instanceof List<?>) {
235            for (Object item : (List<?>) obj) {
236                if (!(item instanceof Dataset)) {
237                    throw new ISOException("Invalid dataset list entry " + item);
238                }
239                datasets.add((Dataset) item);
240            }
241        } else if (obj != null) {
242            throw new ISOException("Unsupported dataset field value " + obj.getClass().getName());
243        }
244    }
245
246    /**
247     * Dataset fields must be packed through their field packager.
248     *
249     * @return never returns normally
250     * @throws ISOException always
251     */
252    @Override
253    public byte[] pack() throws ISOException {
254        throw new ISOException("Not available on Dataset field");
255    }
256
257    /**
258     * Dataset fields must be unpacked through their field packager.
259     *
260     * @param b source buffer
261     * @return never returns normally
262     * @throws ISOException always
263     */
264    @Override
265    public int unpack(byte[] b) throws ISOException {
266        throw new ISOException("Not available on Dataset field");
267    }
268
269    /**
270     * Dataset fields must be unpacked through their field packager.
271     *
272     * @param in source stream
273     * @throws ISOException always
274     */
275    @Override
276    public void unpack(InputStream in) throws ISOException {
277        throw new ISOException("Not available on Dataset field");
278    }
279
280    /**
281     * Dumps the field as dataset-aware XML.
282     *
283     * @param p destination stream
284     * @param indent indentation prefix
285     */
286    @Override
287    public void dump(PrintStream p, String indent) {
288        p.println(indent + "<" + XMLPackager.ISOFIELD_TAG + " " + XMLPackager.ID_ATTR + "=\"" + fieldNumber + "\" type=\"dataset\">");
289        String innerIndent = indent + "  ";
290        for (Dataset dataset : datasets) {
291            p.println(innerIndent + "<dataset id=\"" + String.format("%02X", dataset.getIdentifier()) + "\" format=\"" + dataset.getFormat() + "\">");
292            String datasetIndent = innerIndent + "  ";
293            for (DatasetElement element : dataset.getElements()) {
294                try {
295                    String elementId = dataset.getFormat() == DatasetFormat.TLV
296                      ? String.format("0x%X", element.getId())
297                      : Integer.toString(element.getId());
298                    p.println(datasetIndent
299                      + "<element id=\""
300                      + elementId
301                      + "\""
302                      + (element.isConstructed() ? " constructed=\"true\"" : "")
303                      + " value=\""
304                      + ISOUtil.hexString(element.getBytes())
305                      + "\"/>");
306                } catch (ISOException e) {
307                    p.println(datasetIndent + "<element id=\"" + element.getId() + "\" error=\"" + ISOUtil.normalize(e.getMessage()) + "\"/>");
308                }
309            }
310            p.println(innerIndent + "</dataset>");
311        }
312        p.println(indent + "</" + XMLPackager.ISOFIELD_TAG + ">");
313    }
314}