1   package org.daisy.pipeline.braille.common;
2   
3   import java.io.File;
4   import java.io.FileNotFoundException;
5   import java.io.FileReader;
6   import java.io.IOException;
7   import java.net.URI;
8   import java.util.ArrayList;
9   import java.util.Collections;
10  import java.util.function.Supplier;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.NoSuchElementException;
14  
15  import com.google.common.collect.Iterables;
16  import com.google.common.collect.Maps;
17  
18  import org.daisy.braille.css.LanguageRange;
19  import org.daisy.common.file.URLs;
20  import org.daisy.pipeline.braille.common.Query.MutableQuery;
21  import static org.daisy.pipeline.braille.common.Query.util.mutableQuery;
22  import static org.daisy.pipeline.braille.common.Query.util.query;
23  import static org.daisy.pipeline.braille.common.TransformProvider.util.dispatch;
24  import static org.daisy.pipeline.braille.common.TransformProvider.util.logCreate;
25  import org.daisy.pipeline.braille.common.TransformProvider.util.Memoize;
26  import org.daisy.pipeline.braille.common.util.Files;
27  import org.daisy.pipeline.braille.common.util.Strings;
28  
29  import org.osgi.service.component.annotations.Component;
30  import org.osgi.service.component.annotations.Reference;
31  import org.osgi.service.component.annotations.ReferenceCardinality;
32  import org.osgi.service.component.annotations.ReferencePolicy;
33  
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  @Component(
38  	name = "hyphenator-registry",
39  	service = { HyphenatorRegistry.class }
40  )
41  public class HyphenatorRegistry extends Memoize<Hyphenator> implements HyphenatorProvider<Hyphenator> {
42  
43  	public HyphenatorRegistry() {
44  		super(true); // memoize (id:...) lookups only
45  		providers = new ArrayList<>();
46  		dispatch = dispatch(providers);
47  		context = logger;
48  		unmodifiable = false;
49  	}
50  
51  	private HyphenatorRegistry(TransformProvider<Hyphenator> dispatch,
52  	                           HyphenatorRegistry from,
53  	                           Logger context) {
54  		super(from);
55  		this.providers = null;
56  		this.dispatch = dispatch;
57  		this.context = context;
58  		this.unmodifiable = true;
59  	}
60  
61  	private final List<TransformProvider<Hyphenator>> providers;
62  	private final TransformProvider<Hyphenator> dispatch;
63  	private final Logger context;
64  	private final boolean unmodifiable;
65  
66  	@Reference(
67  		name = "HyphenatorProvider",
68  		unbind = "-",
69  		service = HyphenatorProvider.class,
70  		cardinality = ReferenceCardinality.MULTIPLE,
71  		policy = ReferencePolicy.STATIC
72  	)
73  	/**
74  	 * @throws UnsupportedOperationException if this object is an unmodifiable view of another
75  	 * {@link BrailleTranslatorRegistry}.
76  	 */
77  	public void addProvider(HyphenatorProvider p) {
78  		if (unmodifiable)
79  			throw new UnsupportedOperationException("Unmodifiable");
80  		providers.add(p);
81  	}
82  
83  	public Iterable<Hyphenator> _get(Query q) {
84  		if (q.containsKey("exception-words")) {
85  			MutableQuery baseQuery = mutableQuery(q);
86  			String exceptionsFilePath = baseQuery.removeOnly("exception-words").getValue().get();
87  			FileReader exceptionsFile;
88  			try {
89  				URI u = URLs.asURI(exceptionsFilePath);
90  				if ("volatile-file".equals(u.getScheme()))
91  					// Note that file is compiled every time regardless of whether the scheme is
92  					// "file" or "volatile-file" (see below).
93  					try {
94  						u = new URI("file", u.getSchemeSpecificPart(), u.getFragment());
95  					} catch (Exception e) {
96  						// should not happen
97  						throw new IllegalStateException(e);
98  					}
99  				File f = Files.asFile(u);
100 				if (!f.exists())
101 					throw new FileNotFoundException("File does not exist: " + f);
102 				exceptionsFile = new FileReader(f);
103 			} catch (Exception e) {
104 				context.debug("'exception-words' could not be resolved to a file'", e);
105 				return Collections.emptyList();
106 			}
107 			return Iterables.transform(
108 				_get(baseQuery),
109 				// Note that the word list is compiled again every time unless a `(id:...)' query is given.
110 				h -> {
111 					try {
112 						return logCreate(new HyphenatorWithExceptions(h, exceptionsFile), context);
113 					} catch (IOException e) {
114 						throw new NoSuchElementException(); // should not happen
115 					}
116 				});
117 		}
118 		if (q.containsKey("document-locale")) {
119 			MutableQuery fallbackQuery = mutableQuery(q);
120 			fallbackQuery.removeAll("document-locale");
121 			fallbackQuery.addAll(FALLBACK_QUERY);
122 			return Iterables.concat(
123 				dispatch.get(q),
124 				dispatch.get(fallbackQuery));
125 		} else
126 			return dispatch.get(q);
127 	}
128 
129 	private final static Query FALLBACK_QUERY = query("(document-locale:und)");
130 
131 	@Override
132 	public HyphenatorRegistry withContext(Logger context) {
133 		return (HyphenatorRegistry)super.withContext(context);
134 	}
135 
136 	protected HyphenatorRegistry _withContext(Logger context) {
137 		if (this.context == context)
138 			return this;
139 		return new HyphenatorRegistry(dispatch.withContext(context), this, context);
140 	}
141 
142 	/**
143 	 * Select {@link Hyphenator}s based on a query and a CSS style sheet possibly containing
144 	 * {@code @hyphenation-resource} rules.
145 	 *
146 	 * Contrary to {@link #get(Query)}, this method is not memoized, and the returned objects may
147 	 * not be selectable based on their identifier.
148 	 *
149 	 * @param baseURI Base URI for resolving relative paths in CSS against.
150 	 */
151 	public Iterable<Hyphenator> get(Query query, String style, URI baseURI) {
152 		if (style != null) {
153 			Map<LanguageRange,Query> subQueries = HyphenationResourceParser.getHyphenatorQueries(style, baseURI, query);
154 			if (subQueries != null && !subQueries.isEmpty()) {
155 				Map<LanguageRange,Supplier<Hyphenator>> subHyphenators
156 					= Maps.transformValues(
157 						subQueries,
158 						q -> () -> HyphenatorRegistry.this.get(q).iterator().next());
159 				Iterable<Hyphenator> hyphenators = Iterables.transform(
160 					get(query),
161 					t -> logCreate(new CompoundHyphenator(subHyphenators, t), context));
162 			    if (query.isEmpty())
163 					hyphenators = Iterables.concat(
164 					    hyphenators,
165 						LazyValue.from(() -> logCreate(new CompoundHyphenator(subHyphenators, null), context)));
166 				return hyphenators;
167 			}
168 		}
169 		return get(query);
170 	}
171 
172 	@Override
173 	public String toString() {
174 		return "memoize(dispatch( " + Strings.join(providers, ", ") + " ))";
175 	}
176 
177 	private static final Logger logger = LoggerFactory.getLogger(HyphenatorRegistry.class);
178 }