/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2012
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4che3.data;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.dcm4che3.util.ByteUtils;
import org.dcm4che3.util.ResourceLocator;
import org.dcm4che3.util.StringUtils;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* @author Gunter Zeilinger <gunterze@gmail.com>
*
*/
public class IOD extends ArrayList<IOD.DataElement> {
private static final long serialVersionUID = -5065822488885801576L;
public enum DataElementType {
TYPE_0, TYPE_1, TYPE_2, TYPE_3
}
public static class DataElement implements Serializable {
private static final long serialVersionUID = -7460474415381086525L;
public final int tag;
public final VR vr;
public final DataElementType type;
public final int minVM;
public final int maxVM;
public final int valueNumber;
private Condition condition;
private Object values;
private int lineNumber = -1;
public DataElement(int tag, VR vr, DataElementType type,
int minVM, int maxVM, int valueNumber) {
this.tag = tag;
this.vr = vr;
this.type = type;
this.minVM = minVM;
this.maxVM = maxVM;
this.valueNumber = valueNumber;
}
public DataElement setCondition(Condition condition) {
this.condition = condition;
return this;
}
public Condition getCondition() {
return condition;
}
public int getValueNumber() {
return valueNumber;
}
public DataElement setValues(String... values) {
if (vr == VR.SQ)
throw new IllegalStateException("vr=SQ");
this.values = values;
return this;
}
public DataElement setValues(int... values) {
if (!vr.isIntType())
throw new IllegalStateException("vr=" + vr);
this.values = values;
return this;
}
public DataElement setValues(Code... values) {
if (vr != VR.SQ)
throw new IllegalStateException("vr=" + vr);
this.values = values;
return this;
}
public DataElement addItemIOD(IOD iod) {
if (this.values == null) {
this.values = new IOD[] { iod };
} else {
IOD[] iods = (IOD[]) this.values;
iods = Arrays.copyOf(iods, iods.length+1);
iods[iods.length - 1] = iod;
this.values = iods;
}
return this;
}
public Object getValues() {
return values;
}
public int getLineNumber() {
return lineNumber;
}
public DataElement setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
return this;
}
}
public abstract static class Condition {
protected String id;
protected boolean not;
public Condition id(String id) {
this.id = id;
return this;
}
public final String id() {
return id;
}
public final Condition not() {
this.not = !not;
return this;
}
public abstract boolean match(Attributes attrs);
public void addChild(Condition child) {
throw new UnsupportedOperationException();
}
public Condition trim() {
return this;
}
public boolean isEmpty() {
return false;
}
}
abstract static class CompositeCondition extends Condition {
protected final ArrayList<Condition> childs = new ArrayList<Condition>();
public abstract boolean match(Attributes attrs);
@Override
public void addChild(Condition child) {
childs.add(child);
}
@Override
public Condition trim() {
int size = childs.size();
if (size == 1) {
Condition child = childs.get(0).id(id);
return not ? child.not() : child;
}
childs.trimToSize();
return this;
}
@Override
public boolean isEmpty() {
return childs.isEmpty();
}
}
public static class And extends CompositeCondition {
public boolean match(Attributes attrs) {
for (Condition child : childs) {
if (!child.match(attrs))
return not;
}
return !not;
}
}
public static class Or extends CompositeCondition {
public boolean match(Attributes attrs) {
for (Condition child : childs) {
if (child.match(attrs))
return !not;
}
return not;
}
}
public static class Present extends Condition {
protected final int tag;
protected final int[] itemPath;
public Present(int tag, int... itemPath) {
this.tag = tag;
this.itemPath = itemPath;
}
public boolean match(Attributes attrs) {
return not ? !item(attrs).containsValue(tag)
: item(attrs).containsValue(tag);
}
protected Attributes item(Attributes attrs) {
for (int sqtag : itemPath) {
if (sqtag == -1)
attrs = (sqtag == -1)
? attrs.getParent()
: attrs.getNestedDataset(sqtag);
}
return attrs;
}
}
public static class MemberOf extends Present {
private final VR vr;
private final int valueIndex;
private final boolean matchNotPresent;
private Object values;
public MemberOf(int tag, VR vr, int valueIndex,
boolean matchNotPresent, int... itemPath) {
super(tag, itemPath);
this.vr = vr;
this.valueIndex = valueIndex;
this.matchNotPresent = matchNotPresent;
}
public VR vr() {
return vr;
}
public MemberOf setValues(String... values) {
if (vr == VR.SQ)
throw new IllegalStateException("vr=SQ");
this.values = values;
return this;
}
public MemberOf setValues(int... values) {
if (!vr.isIntType())
throw new IllegalStateException("vr=" + vr);
this.values = values;
return this;
}
public MemberOf setValues(Code... values) {
if (vr != VR.SQ)
throw new IllegalStateException("vr=" + vr);
this.values = values;
return this;
}
public boolean match(Attributes attrs) {
if (values == null)
throw new IllegalStateException("values not initialized");
Attributes item = item(attrs);
if (item == null)
return matchNotPresent;
if (values instanceof int[])
return not ? !match(item, ((int[]) values))
: match(item, ((int[]) values));
else if (values instanceof Code[])
return not ? !match(item, ((Code[]) values))
: match(item, ((Code[]) values));
else
return not ? !match(item, ((String[]) values))
: match(item, ((String[]) values));
}
private boolean match(Attributes item, String[] ss) {
String val = item.getString(tag, valueIndex);
if (val == null)
return not ? !matchNotPresent : matchNotPresent;
for (String s : ss) {
if (s.equals(val))
return !not;
}
return not;
}
private boolean match(Attributes item, Code[] codes) {
Sequence seq = item.getSequence(tag);
if (seq != null)
for (Attributes codeItem : seq) {
try {
Code val = new Code(codeItem);
for (Code code : codes) {
if (code.equals(val))
return !not;
}
} catch (NullPointerException npe) {}
}
return not;
}
private boolean match(Attributes item, int[] is) {
int val = item.getInt(tag, valueIndex, Integer.MIN_VALUE);
if (val == Integer.MIN_VALUE)
return matchNotPresent;
for (int i : is) {
if (i == val)
return true;
}
return false;
}
}
private DataElementType type;
private Condition condition;
private int lineNumber = -1;
public void setType(DataElementType type) {
this.type = type;
}
public DataElementType getType() {
return type;
}
public void setCondition(Condition condition) {
this.condition = condition;
}
public Condition getCondition() {
return condition;
}
public int getLineNumber() {
return lineNumber;
}
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
public void parse(String uri) throws IOException {
try {
SAXParserFactory f = SAXParserFactory.newInstance();
SAXParser parser = f.newSAXParser();
parser.parse(uri, new SAXHandler(this));
} catch (SAXException e) {
throw new IOException("Failed to parse " + uri, e);
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
private static class SAXHandler extends DefaultHandler {
private StringBuilder sb = new StringBuilder();
private boolean processCharacters;
private boolean elementConditions;
private boolean itemConditions;
private String idref;
private List<String> values = new ArrayList<String>();
private List<Code> codes = new ArrayList<Code>();
private LinkedList<IOD> iodStack = new LinkedList<IOD>();
private LinkedList<Condition> conditionStack = new LinkedList<Condition>();
private Map<String, IOD> id2iod = new HashMap<String, IOD>();
private Map<String, Condition> id2cond = new HashMap<String, Condition>();
private Locator locator;
public SAXHandler(IOD iod) {
iodStack.add(iod);
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String qName,
org.xml.sax.Attributes atts) throws SAXException {
switch (qName.charAt(0)) {
case 'A':
if (qName.equals("And"))
startCondition(qName, new And());
break;
case 'C':
if (qName.equals("Code"))
startCode(
atts.getValue("codeValue"),
atts.getValue("codingSchemeDesignator"),
atts.getValue("codingSchemeVersion"),
atts.getValue("codeMeaning"));
case 'D':
if (qName.equals("DataElement"))
startDataElement(
atts.getValue("tag"),
atts.getValue("vr"),
atts.getValue("type"),
atts.getValue("vm"),
atts.getValue("items"),
atts.getValue("valueNumber"));
break;
case 'I':
if (qName.equals("If"))
startIf(atts.getValue("id"), atts.getValue("idref"));
else if (qName.equals("Item"))
startItem(atts.getValue("id"),
atts.getValue("idref"),
atts.getValue("type"));
break;
case 'M':
if (qName.equals("MemberOf"))
startCondition(qName, memberOf(atts));
break;
case 'N':
if (qName.equals("NotAnd"))
startCondition(qName, new And().not());
else if (qName.equals("NotMemberOf"))
startCondition(qName, memberOf(atts).not());
else if (qName.equals("NotOr"))
startCondition(qName, new Or().not());
else if (qName.equals("NotPresent"))
startCondition(qName, present(atts).not());
break;
case 'O':
if (qName.equals("Or"))
startCondition(qName, new Or());
break;
case 'P':
if (qName.equals("Present"))
startCondition(qName, present(atts));
break;
case 'V':
if (qName.equals("Value"))
startValue();
break;
}
}
private Present present(org.xml.sax.Attributes atts)
throws SAXException {
int[] tagPath = tagPathOf(atts.getValue("tag"));
int lastIndex = tagPath.length-1;
return new Present(tagPath[lastIndex],
lastIndex > 0 ? Arrays.copyOf(tagPath, lastIndex)
: ByteUtils.EMPTY_INTS);
}
private MemberOf memberOf(org.xml.sax.Attributes atts)
throws SAXException {
int[] tagPath = tagPathOf(atts.getValue("tag"));
int lastIndex = tagPath.length-1;
return new MemberOf(
tagPath[lastIndex],
vrOf(atts.getValue("vr")),
valueNumberOf(atts.getValue("valueNumber"), 1) - 1,
matchNotPresentOf(atts.getValue("matchNotPresent")),
lastIndex > 0 ? Arrays.copyOf(tagPath, lastIndex)
: ByteUtils.EMPTY_INTS);
}
private void startCode(String codeValue,
String codingSchemeDesignator,
String codingSchemeVersion,
String codeMeaning) throws SAXException {
if (codeValue == null)
throw new SAXException("missing codeValue attribute");
if (codingSchemeDesignator == null)
throw new SAXException("missing codingSchemeDesignator attribute");
if (codeMeaning == null)
throw new SAXException("missing codeMeaning attribute");
codes.add(new Code(codeValue, codingSchemeDesignator,
codingSchemeVersion, codeMeaning));
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
switch (qName.charAt(0)) {
case 'A':
if (qName.equals("And"))
endCondition(qName);
break;
case 'D':
if (qName.equals("DataElement"))
endDataElement();
break;
case 'I':
if (qName.equals("If"))
endCondition(qName);
else if (qName.equals("Item"))
endItem();
break;
case 'M':
if (qName.equals("MemberOf"))
endCondition(qName);
break;
case 'N':
if (qName.equals("NotAnd"))
endCondition(qName);
else if (qName.equals("NotMemberOf"))
endCondition(qName);
else if (qName.equals("NotOr"))
endCondition(qName);
else if (qName.equals("NotPresent"))
endCondition(qName);
break;
case 'O':
if (qName.equals("Or"))
endCondition(qName);
break;
case 'P':
if (qName.equals("Present"))
endCondition(qName);
break;
case 'V':
if (qName.equals("Value"))
endValue();
break;
}
processCharacters = false;
idref = null;
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
if (processCharacters)
sb.append(ch, start, length);
}
private void startDataElement(String tagStr, String vrStr,
String typeStr, String vmStr, String items,
String valueNumberStr) throws SAXException {
if (idref != null)
throw new SAXException("<Item> with idref must be empty");
IOD iod = iodStack.getLast();
int tag = tagOf(tagStr);
VR vr = vrOf(vrStr);
DataElementType type = typeOf(typeStr);
int minVM = -1;
int maxVM = -1;
String vm = vr == VR.SQ ? items : vmStr;
if (vm != null) {
try {
String[] ss = StringUtils.split(vm, '-');
if (ss[0].charAt(0) != 'n') {
minVM = Integer.parseInt(ss[0]);
if (ss.length > 1) {
if (ss[1].charAt(0) != 'n')
maxVM = Integer.parseInt(ss[1]);
} else {
maxVM = minVM;
}
}
} catch (IllegalArgumentException e) {
throw new SAXException(
(vr == VR.SQ ? "invalid items=\""
: "invalid vm=\"")
+ vm + '"');
}
}
DataElement el = new DataElement(tag, vr, type, minVM, maxVM,
valueNumberOf(valueNumberStr, 0));
if (locator != null)
el.setLineNumber(locator.getLineNumber());
iod.add(el);
elementConditions = true;
itemConditions = false;
}
private DataElementType typeOf(String s) throws SAXException {
if (s == null)
throw new SAXException("missing type attribute");
try {
return DataElementType.valueOf("TYPE_" + s);
} catch (IllegalArgumentException e) {
throw new SAXException("unrecognized type=\"" + s + '"');
}
}
private VR vrOf(String s) throws SAXException {
try {
return VR.valueOf(s);
} catch (NullPointerException e) {
throw new SAXException("missing vr attribute");
} catch (IllegalArgumentException e) {
throw new SAXException("unrecognized vr=\"" + s + '"');
}
}
private int tagOf(String s) throws SAXException {
try {
return (int) Long.parseLong(s, 16);
} catch (NullPointerException e) {
throw new SAXException("missing tag attribute");
} catch (IllegalArgumentException e) {
throw new SAXException("invalid tag=\"" + s + '"');
}
}
private int[] tagPathOf(String s) throws SAXException {
String[] ss = StringUtils.split(s, '/');
if (ss.length == 0)
throw new SAXException("missing tag attribute");
try {
int[] tagPath = new int[ss.length];
for (int i = 0; i < tagPath.length; i++)
tagPath[i] = ss[i].equals("..")
? -1
: (int) Long.parseLong(s, 16);
return tagPath;
} catch (IllegalArgumentException e) {
throw new SAXException("invalid tag=\"" + s + '"');
}
}
private int valueNumberOf(String s, int def) throws SAXException {
try {
return s != null ? Integer.parseInt(s) : def;
} catch (IllegalArgumentException e) {
throw new SAXException("invalid valueNumber=\"" + s + '"');
}
}
private boolean matchNotPresentOf(String s) {
return s != null && s.equalsIgnoreCase("true");
}
private DataElement getLastDataElement() {
IOD iod = iodStack.getLast();
return iod.get(iod.size()-1);
}
private void endDataElement() throws SAXException {
DataElement el = getLastDataElement();
if (!values.isEmpty()) {
try {
if (el.vr.isIntType())
el.setValues(parseInts(values));
else
el.setValues(values.toArray(new String[values.size()]));
} catch (IllegalStateException e) {
throw new SAXException("unexpected <Value>");
}
values.clear();
}
if (!codes.isEmpty()) {
try {
el.setValues(codes.toArray(new Code[codes.size()]));
} catch (IllegalStateException e) {
throw new SAXException("unexpected <Code>");
}
codes.clear();
}
elementConditions = false;
}
private int[] parseInts(List<String> list) {
int[] is = new int[list.size()];
for (int i = 0; i < is.length; i++)
is[i] = Integer.parseInt(list.get(i));
return is;
}
private void startValue() {
sb.setLength(0);
processCharacters = true;
}
private void endValue() {
values.add(sb.toString());
}
private void startItem(String id, String idref, String type) throws SAXException {
IOD iod;
if (idref != null) {
if (type != null)
throw new SAXException("<Item> with idref must not specify type");
iod = id2iod.get(idref);
if (iod == null)
throw new SAXException(
"could not resolve <Item idref:\"" + idref + "\"/>");
} else {
iod = new IOD();
if (type != null)
iod.setType(typeOf(type));
if (locator != null)
iod.setLineNumber(locator.getLineNumber());
}
getLastDataElement().addItemIOD(iod);
iodStack.add(iod);
if (id != null)
id2iod.put(id, iod);
this.idref = idref;
itemConditions = true;
elementConditions = false;
}
private void endItem() {
iodStack.removeLast().trimToSize();
itemConditions = false;
}
private void startIf(String id, String idref) throws SAXException {
if (!conditionStack.isEmpty())
throw new SAXException("unexpected <If>");
Condition cond;
if (idref != null) {
cond = id2cond.get(idref);
if (cond == null)
throw new SAXException(
"could not resolve <If idref:\"" + idref + "\"/>");
} else {
cond = new And().id(id);
}
conditionStack.add(cond);
if (id != null)
id2cond.put(id, cond);
this.idref = idref;
}
private void startCondition(String name, Condition cond)
throws SAXException {
if (!(elementConditions || itemConditions))
throw new SAXException("unexpected <" + name + '>');
conditionStack.add(cond);
}
private void endCondition(String name) throws SAXException {
Condition cond = conditionStack.removeLast();
if (cond.isEmpty())
throw new SAXException('<' + name + "> must not be empty");
if (!values.isEmpty()) {
try {
MemberOf memberOf = (MemberOf) cond;
if (memberOf.vr.isIntType())
memberOf.setValues(parseInts(values));
else
memberOf.setValues(values.toArray(new String[values.size()]));
} catch (Exception e) {
throw new SAXException("unexpected <Value> contained by <"
+ name + ">");
}
values.clear();
}
if (!codes.isEmpty()) {
try {
((MemberOf) cond).setValues(codes.toArray(new Code[codes.size()]));
} catch (Exception e) {
throw new SAXException("unexpected <Code> contained by <"
+ name + ">");
}
codes.clear();
}
if (conditionStack.isEmpty()) {
if (elementConditions)
getLastDataElement().setCondition(cond.trim());
else
iodStack.getLast().setCondition(cond.trim());
elementConditions = false;
itemConditions = false;
} else
conditionStack.getLast().addChild(cond.trim());
}
}
public static IOD load(String uri) throws IOException {
if (uri.startsWith("resource:")) {
try {
uri = ResourceLocator.getResource(uri.substring(9), IOD.class);
} catch (NullPointerException npe) {
throw new FileNotFoundException(uri);
}
} else if (uri.indexOf(':') < 2) {
uri = new File(uri).toURI().toString();
}
IOD iod = new IOD();
iod.parse(uri);
iod.trimToSize();
return iod;
}
public static IOD valueOf(Code code) {
IOD iod = new IOD();
iod.add(new DataElement(
Tag.CodeValue, VR.SH, DataElementType.TYPE_1, 1, 1, 0)
.setValues(code.getCodeValue()));
iod.add(new DataElement(
Tag.CodingSchemeDesignator, VR.SH, DataElementType.TYPE_1, 1, 1, 0)
.setValues(code.getCodingSchemeDesignator()));
String codingSchemeVersion = code.getCodingSchemeVersion();
if (codingSchemeVersion == null)
iod.add(new DataElement(
Tag.CodingSchemeVersion, VR.SH, DataElementType.TYPE_0, -1, -1, 0));
else
iod.add(new DataElement(
Tag.CodingSchemeVersion, VR.SH, DataElementType.TYPE_1, 1, 1, 0));
return iod;
}
}