/******************************************************************************* * Copyright 2013 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.model.core.shader.include; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import au.gov.ga.earthsci.worldwind.common.util.Util; /** * A simple shader {@code #include} processor that scans for {@code #include} * directives and returns a version of the shader text with the {@code #include} * directive replaced with the appropriate text. * <p/> * The processor supports sourcing of replacement strings from two locations: * <ol> * <li>Using the standard Java resource loading mechanism (see * {@link Class#getResource(String)}); or * <li>Using a named string constant that must be provided to the processor * using {@link #namedString(String, String)} * </ol> * * @author James Navin (james.navin@ga.gov.au) * */ public class ShaderIncludeProcessor { private static final Logger logger = LoggerFactory.getLogger(ShaderIncludeProcessor.class); private static final Pattern INCLUDE_PATTERN = Pattern.compile("\\s*#include (\\S+)"); //$NON-NLS-1$ private final Map<String, String> namedStrings = new HashMap<String, String>(); /** * Register a named string with the processor for inclusion in the directive * replacement process. * * @param name * The name to register the replacement string under * @param value * The value to use if this name is encountered */ public void namedString(String name, String value) { namedStrings.put(name, value); } /** * Remove the named string from this processor. * * @param name * The named string to remove */ public void deleteNamedString(String name) { namedStrings.remove(name); } /** * Equivalent to {@link #processResource(Class, String, false)} * * @param loader * The loader to use for loading resource * @param name * The name of the resource to load and process * @return The processed resource, or <code>null</code> if no resource with * the given name could be found. * @throws IOException */ public String processResource(Class<?> loader, String name) throws IOException { return processResource(loader, name, false); } /** * Process the resource with the given name. * <p/> * Uses the standard java resource loading mechanism, with the provided * {@link Class} serving as the resource loader. * * @param loader * The class to use for loading resources * @param name * The name of the shader resource to process * @param failQuietly * If <code>true</code>, {@code #includes} that are unable to be * processed will cause an exception. If <code>false</code>, the * missing {@code #includes} will be replaced with the empty * string. * * @return The processed resource; or <code>null</code> if no resource could * be found. * @throws IOException * If failQuietly is <code>false</code> and a problem occurs * processing the resource and/or its {@code #includes} * * @see ShaderIncludeProcessor#process(String, boolean) */ public String processResource(Class<?> loader, String name, boolean failQuietly) throws IOException { if (loader == null) { loader = getClass(); } if (name == null) { if (!failQuietly) { throw new IOException("Unable to open resource null"); //$NON-NLS-1$ } return null; } String resource = null; try { InputStream stream = loader.getResourceAsStream(name); if (stream == null) { if (!failQuietly) { throw new IOException("Unable to open resource " + name); //$NON-NLS-1$ } return null; } resource = Util.readStreamToString(stream); } catch (IOException e) { logger.debug("Unable to process resource " + name, e); //$NON-NLS-1$ if (!failQuietly) { throw e; } } catch (Exception e) { logger.debug("Unable to process resource " + name, e); //$NON-NLS-1$ } if (resource == null) { return null; } return process(resource, failQuietly); } /** * Process the given source string and return the result. * <p/> * Loaded replacements will be processed recursively (e.g. replacement * strings that include {@code #include} will be expanded prior to inclusion * in the output). * <p/> * If any includes are unable to be processed, an exception will be * generated. * * @param source * The source string to process * @return The source string, with {@code #include} directives replaced with * appropriate content. * * @throws IOException * If something goes wrong when reading replacements * * @see #process(String, boolean) */ public String process(String source) throws IOException { return process(source, false); } /** * Process the given source string and return the result. * <p/> * Loaded replacements will be processed recursively (e.g. replacement * strings that include {@code #include} will be expanded prior to inclusion * in the output). * <p/> * If any includes are unable to be processed, and {@code failQuietly} is * <code>false</code>, an exception will be generated. Otherwise the include * will be replaced with the empty string. * * @param source * The source string to process * @param failQuietly * If <code>true</code>, includes that cannot be processed will * be replaced with the empty string; if <code>false</code>, * exceptions will be generated. * * @return The source string, with {@code #include} directives replaced with * appropriate content. * * @throws IOException * If something goes wrong when reading replacements * * @see #process(String, boolean) */ public String process(String source, boolean failQuietly) throws IOException { if (source == null) { return null; } if (source.length() == 0) { return source; } StringBuffer result = new StringBuffer(); // Process line at a time; String line; BufferedReader reader = new BufferedReader(new StringReader(source)); while ((line = reader.readLine()) != null) { result.append('\n'); if (isInclude(line)) { String name = getIncludeName(line); String substitute; try { substitute = getSubstitute(name); result.append(substitute); } catch (IOException e) { if (!failQuietly) { throw e; } } } else { result.append(line); } } return result.substring(1); } private boolean isInclude(String line) { return INCLUDE_PATTERN.matcher(line.trim()).matches(); } private String getIncludeName(String line) { Matcher m = INCLUDE_PATTERN.matcher(line.trim()); m.find(); String result = m.group(1); return result; } private String getSubstitute(String name) throws IOException { // First check named strings if (namedStrings.containsKey(name)) { return process(namedStrings.get(name)); } // Otherwise try and load a resource try { String includedSource = Util.readStreamToString(getClass().getResourceAsStream(name)); if (includedSource == null) { throw new IOException("No include found with name \"" + name + "\""); //$NON-NLS-1$ //$NON-NLS-2$ } return process(includedSource); } catch (Exception e) { throw new IOException("Unable to load include \"" + name + "\"", e); //$NON-NLS-1$ //$NON-NLS-2$ } } }