1 package org.daisy.pipeline.braille.common;
2
3 import java.util.AbstractCollection;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.Iterator;
7 import java.util.List;
8 import java.util.ListIterator;
9 import java.util.NoSuchElementException;
10 import java.util.Optional;
11 import java.util.regex.Matcher;
12 import java.util.regex.Pattern;
13
14 import javax.xml.namespace.QName;
15 import javax.xml.stream.XMLStreamException;
16 import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
17 import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
18 import javax.xml.stream.XMLStreamReader;
19 import javax.xml.stream.XMLStreamWriter;
20
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.Iterables;
23
24 import static com.xmlcalabash.core.XProcConstants.NS_XPROC_STEP;
25
26 import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
27 import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
28
29 import org.unbescape.css.CssEscape;
30
31 public interface Query extends Iterable<Query.Feature> {
32
33 @Override
34 public String toString();
35
36 public boolean containsKey(String key);
37 public Iterable<Feature> get(String key);
38 public Feature getOnly(String key) throws IllegalStateException;
39 public boolean isEmpty();
40
41 public static interface Feature {
42 public String getKey();
43 public boolean hasValue();
44 public Optional<String> getValue();
45 public Optional<String> getLiteral();
46 @Override public String toString();
47 }
48
49 public static interface MutableQuery extends Query {
50 public MutableQuery add(String key);
51 public MutableQuery add(String key, String value);
52 public MutableQuery add(String key, Optional<String> value);
53 public boolean add(Feature feature);
54 public MutableQuery addAll(Iterable<Feature> features);
55 public boolean remove(Feature feature);
56 public Iterable<Feature> removeAll(String key);
57 public Feature removeOnly(String key) throws IllegalStateException;
58 public Query asImmutable();
59 }
60
61
62
63
64
65 public static abstract class util {
66
67
68
69 public static Query query(String query) throws IllegalArgumentException {
70 if (FEATURES_RE.matcher(query).matches()) {
71 MutableQuery mq = mutableQuery();
72 Matcher m = FEATURE_RE.matcher(query);
73 while(m.find()) {
74 String key = m.group("key");
75 String value = m.group("value");
76 boolean isString = false;
77 if (value != null) {
78 Matcher m2 = VALUE_RE.matcher(value);
79 if (!m2.matches())
80 throw new RuntimeException("Coding error");
81 String ident = m2.group("ident");
82 String string = m2.group("string");
83 String integer = m2.group("integer");
84 if (ident != null)
85 value = ident;
86 else if (string != null && !string.equals("")) {
87 value = CssEscape.unescapeCss(string.substring(1, string.length() - 1));
88 isString = true; }
89 else if (integer != null && !integer.equals(""))
90 value = integer;
91 else
92 throw new RuntimeException("Coding error"); }
93 mq.add(new FeatureImpl(key, Optional.ofNullable(value), isString)); }
94 return mq.asImmutable(); }
95 throw new IllegalArgumentException("Could not parse query: " + query);
96 }
97
98 public static Query query(XMLStreamReader query) throws XMLStreamException {
99 return unmarshallQuery(query);
100 }
101
102
103
104 public static MutableQuery mutableQuery() {
105 return new MutableQueryImpl();
106 }
107
108 public static MutableQuery mutableQuery(Query copyOf) {
109 MutableQuery q = new MutableQueryImpl();
110 q.addAll(copyOf);
111 return q;
112 }
113
114
115
116 private static final QName C_PARAM_SET = new QName(NS_XPROC_STEP, "param-set", "c");
117 private static final QName C_PARAM = new QName(NS_XPROC_STEP, "param", "c");
118 private static final QName _NAME = new QName("name");
119 private static final QName _NAMESPACE = new QName("namespace");
120 private static final QName _VALUE = new QName("value");
121
122
123
124
125 public static void marshallQuery(Query query, XMLStreamWriter writer) throws XMLStreamException {
126 writeStartElement(writer, C_PARAM_SET);
127 for (Feature f : query) {
128 writeStartElement(writer, C_PARAM);
129 writeAttribute(writer, _NAME, f.getKey());
130 writeAttribute(writer, _NAMESPACE, "");
131 writeAttribute(writer, _VALUE, f.getValue().orElse("true"));
132 writer.writeEndElement();
133 }
134 writer.writeEndElement();
135 }
136
137
138
139
140 public static Query unmarshallQuery(XMLStreamReader reader) throws XMLStreamException {
141 MutableQuery query = mutableQuery();
142 int depth = 0;
143 int event = reader.next();
144 while (true)
145 try {
146 switch (event) {
147 case START_ELEMENT:
148 if (depth == 0 && C_PARAM.equals(reader.getName())) {
149 String name = null;
150 String namespace = null;
151 String value = null;
152 for (int i = 0; i < reader.getAttributeCount(); i++) {
153 QName attrName = reader.getAttributeName(i);
154 String attrValue = reader.getAttributeValue(i);
155 if (_NAME.equals(attrName))
156 name = attrValue;
157 else if (_NAMESPACE.equals(attrName))
158 namespace = attrValue;
159 else if (_VALUE.equals(attrName))
160 value = attrValue;
161 }
162 if ((namespace == null || "".equals(namespace)) && name != null && value != null)
163 query.add(name, value);
164 }
165 depth++;
166 break;
167 case END_ELEMENT:
168 depth--;
169 break;
170 default:
171 }
172 event = reader.next();
173 } catch (NoSuchElementException e) {
174 break;
175 }
176 return query.asImmutable();
177 }
178
179 private static abstract class AbstractQueryImpl extends AbstractCollection<Feature> implements Query {
180
181 protected final List<Feature> list;
182
183 protected AbstractQueryImpl() {
184 this(new ArrayList<Feature>());
185 }
186
187 protected AbstractQueryImpl(List<Feature> list) {
188 this.list = list;
189 }
190
191 public int size() {
192 return list.size();
193 }
194
195 public Iterator<Feature> iterator() {
196 return list.iterator();
197 }
198
199 public boolean containsKey(String key) {
200 return get(key).iterator().hasNext();
201 }
202
203 public Iterable<Feature> get(final String key) {
204 final ListIterator<Feature> features = list.listIterator();
205 while (features.hasNext()) {
206 Feature next = features.next();
207 if (next.getKey().equals(key)) {
208 features.previous();
209 return new Iterable<Feature>() {
210 public Iterator<Feature> iterator() {
211 return new Iterator<Feature>() {
212 private boolean canRemove = false;
213 public boolean hasNext() {
214 return features.hasNext() && list.get(features.nextIndex()).getKey().equals(key);
215 }
216 public Feature next() {
217 Feature next = features.next();
218 if (next.getKey().equals(key)) {
219 canRemove = true;
220 return next; }
221 else
222 throw new NoSuchElementException();
223 }
224 public void remove() {
225 features.remove();
226 canRemove = false;
227 }
228 };
229 }
230 };
231 }
232 }
233 return Collections.emptyList();
234 }
235
236 public Feature getOnly(String key) throws IllegalStateException {
237 Iterator<Feature> features = get(key).iterator();
238 if (!features.hasNext())
239 throw new IllegalStateException();
240 Feature f = features.next();
241 if (features.hasNext())
242 throw new IllegalStateException();
243 return f;
244 }
245
246 public String toString() {
247 StringBuilder b = new StringBuilder();
248 for (Feature f : this)
249 b.append(f);
250 return b.toString();
251 }
252
253 @Override
254 public int hashCode() {
255 return list.hashCode();
256 }
257
258 @Override
259 public boolean equals(Object obj) {
260 if (this == obj)
261 return true;
262 if (obj == null)
263 return false;
264 if (!(obj instanceof Query))
265 return false;
266 return Iterables.elementsEqual(this, (Query)obj);
267 }
268 }
269
270 private static class MutableQueryImpl extends AbstractQueryImpl implements MutableQuery {
271
272 public boolean add(Feature feature) {
273 int index = 0;
274 String key = feature.getKey();
275 for (Feature f : list) {
276 int cmp = f.getKey().compareTo(key);
277 if (cmp > 0) {
278 break;
279 } else if (cmp > 0) {
280 index++;
281 break;
282 }
283 index++;
284 }
285 list.add(index, feature);
286 return true;
287 }
288
289 public MutableQuery add(String key) {
290 add(key, Optional.<String>empty());
291 return this;
292 }
293
294 public MutableQuery add(String key, String value) {
295 add(key, Optional.<String>ofNullable(value));
296 return this;
297 }
298
299 public MutableQuery add(String key, Optional<String> value) {
300 add(new FeatureImpl(key, value));
301 return this;
302 }
303
304 public MutableQuery addAll(Iterable<Feature> features) {
305 for (Feature f : features)
306 add(f);
307 return this;
308 }
309
310 public boolean remove(Feature feature) {
311 return super.remove(feature);
312 }
313
314 public Iterable<Feature> removeAll(final String key) {
315 ImmutableList.Builder<Feature> list = new ImmutableList.Builder<Feature>();
316 Iterator<Feature> features = iterator();
317 while (features.hasNext()) {
318 Feature next = features.next();
319 if (next.getKey().equals(key)) {
320 features.remove();
321 list.add(next); }}
322 return list.build();
323 }
324
325 public Feature removeOnly(String key) throws IllegalStateException {
326 Iterator<Feature> features = get(key).iterator();
327 if (!features.hasNext())
328 throw new IllegalStateException();
329 Feature f = features.next();
330 if (features.hasNext())
331 throw new IllegalStateException();
332 features.remove();
333 return f;
334 }
335
336
337
338
339 public Query asImmutable() {
340 return new ImmutableQueryImpl(this);
341 }
342 }
343
344 private static class ImmutableQueryImpl extends AbstractQueryImpl {
345
346 private ImmutableQueryImpl(MutableQueryImpl query) {
347 super(ImmutableList.copyOf(query.list));
348 }
349 }
350
351 private static class FeatureImpl implements Feature {
352
353 final String key;
354 final Optional<String> value;
355 final Optional<String> literal;
356
357 private FeatureImpl(String key, Optional<String> value) {
358 this(key, value, false);
359 }
360
361 private FeatureImpl(String key, Optional<String> value, boolean specifiedAsString) {
362 this.key = key;
363 this.value = value;
364 if (value.isPresent()) {
365 String v = value.get();
366 if (!specifiedAsString && (v.matches(IDENT_RE) || v.matches(INTEGER_RE)))
367 this.literal = Optional.of(v);
368 else
369 this.literal = Optional.of("\"" + v.replace("\n", "\\A ").replace("\"","\\22 ") + "\"");
370 } else
371 this.literal = Optional.empty();
372 }
373
374 public String getKey() {
375 return key;
376 }
377
378 public boolean hasValue() {
379 return getValue().isPresent();
380 }
381
382 public Optional<String> getValue() {
383 return value;
384 }
385
386 public Optional<String> getLiteral() {
387 return literal;
388 }
389
390 public String toString() {
391 StringBuilder b = new StringBuilder();
392 String k = getKey();
393 if (!k.matches(IDENT_RE))
394 throw new RuntimeException();
395 b.append("(" + k);
396 if (hasValue()) {
397 b.append(":");
398 b.append(getLiteral().get());
399 }
400 b.append(")");
401 return b.toString();
402 }
403
404 @Override
405 public int hashCode() {
406 final int prime = 31;
407 int result = 1;
408 result = prime * result + ((key == null) ? 0 : key.hashCode());
409 result = prime * result + ((value == null) ? 0 : value.hashCode());
410 return result;
411 }
412
413 @Override
414 public boolean equals(Object obj) {
415 if (this == obj)
416 return true;
417 if (obj == null)
418 return false;
419 if (getClass() != obj.getClass())
420 return false;
421 Feature other = (Feature)obj;
422 if (key == null) {
423 if (other.getKey() != null)
424 return false;
425 } else if (!key.equals(other.getKey()))
426 return false;
427 if (value == null) {
428 if (other.getValue() != null)
429 return false;
430 } else if (!value.equals(other.getValue()))
431 return false;
432 return true;
433 }
434 }
435
436 private static final String IDENT_RE = "[_a-zA-Z][_a-zA-Z0-9-]*";
437 private static final String STRING_RE = "'[^']*'|\"[^\"]*\"";
438 private static final String INTEGER_RE = "0|-?[1-9][0-9]*";
439 private static final Pattern VALUE_RE = Pattern.compile(
440 "(?<ident>" + IDENT_RE + ")|(?<string>" + STRING_RE + ")|(?<integer>" + INTEGER_RE + ")"
441 );
442 private static final Pattern FEATURE_RE = Pattern.compile(
443 "\\(\\s*(?<key>" + IDENT_RE+ ")(?:\\s*\\:\\s*(?<value>" + VALUE_RE.pattern() + "))?\\s*\\)"
444 );
445 private static final Pattern FEATURES_RE = Pattern.compile(
446 "\\s*(?:" + FEATURE_RE.pattern() + "\\s*)*"
447 );
448 public static final Pattern QUERY = FEATURES_RE;
449 }
450 }