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  	/*       UTILS        */
63  	/* ================== */
64  	
65  	public static abstract class util {
66  		
67  		/* query() */
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 		/* mutableQuery() */
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 		/* marshallQuery & unmarshallQuery */
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 		 * Convert query to c:param-set document
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 		 * Convert c:param-set document to query
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 			 * Returned value is a snapshot, i.e. will not change after the call.
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 }