/*
* #%L
* carewebframework
* %%
* Copyright (C) 2008 - 2016 Regenstrief Institute, Inc.
* %%
* 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.
*
* This Source Code Form is also subject to the terms of the Health-Related
* Additional Disclaimer of Warranty and Limitation of Liability available at
*
* http://www.carewebframework.org/licensing/disclaimer.
*
* #L%
*/
package org.carewebframework.maven.plugin.theme;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.carewebframework.maven.plugin.core.BaseMojo;
import org.carewebframework.maven.plugin.resource.IResource;
import org.carewebframework.maven.plugin.transform.AbstractTransform;
import org.codehaus.plexus.util.StringUtils;
/**
* Applies a map template to the source CSS to generate additional CSS.
*/
public class CSSTransform extends AbstractTransform {
public CSSTransform(BaseMojo mojo) {
super(mojo);
}
private static final String DELIM = "|";
private final Map<String, String> srcMap = new LinkedHashMap<>();
private final Map<String, String> defMap = new LinkedHashMap<>();
@Override
public void transform(IResource resource, OutputStream outputStream) throws Exception {
CSSResource css = (CSSResource) resource;
File mapper = css.getMapper();
if (mapper == null) {
super.transform(resource, outputStream);
return;
}
try (InputStream in = new FileInputStream(mapper)) {
LineIterator lines = IOUtils.lineIterator(in, "UTF-8");
String line = "";
while (lines.hasNext()) {
line += lines.nextLine();
if (line.endsWith("\\")) {
line = StringUtils.left(line, line.length() - 1);
} else {
addMapEntry(line);
line = "";
}
}
addMapEntry(line);
}
super.transform(resource, outputStream);
}
/**
* Parses and adds the entry to the source or default map.
*
* @param s String entry.
*/
private void addMapEntry(String s) {
String[] pcs = s.split("\\=", 2);
if (pcs.length == 2) {
String sel = pcs[0].trim();
boolean deflt = sel.startsWith("!");
Map<String, String> map = deflt ? defMap : srcMap;
sel = deflt ? sel.substring(1) : sel;
if (map.containsKey(sel)) {
map.put(sel, map.get(sel) + pcs[1]);
} else {
map.put(sel, pcs[1]);
}
}
}
/**
* Checks the selector for a match in either the source map or the reference map. If found in
* the source map, it is moved to the reference map and added to the matches set. If found in
* the reference map, it is added to the matches set. If not found, no action is taken.
*
* @param sel The CSS selector.
* @param matches The matches set.
* @param refMap The map of referenced selectors.
*/
private void checkForMatch(String sel, Set<String> matches, Map<String, String> refMap) {
sel = sel.trim();
if (srcMap.containsKey(sel)) {
matches.add(sel);
defMap.remove(sel);
refMap.put(sel, srcMap.remove(sel));
} else if (refMap.containsKey(sel)) {
matches.add(sel);
}
}
/**
* Writes the map contents to the output stream.
*
* @param map Map to write.
* @throws IOException IO exception.
*/
private void writeMap(Map<String, String> map, OutputStream outputStream) throws IOException {
for (String entry : map.values()) {
if (entry.contains(DELIM)) {
mojo.getLog().warn("Output contains unresolved reference: " + entry);
} else {
outputStream.write(entry.getBytes());
}
}
}
@Override
public void transform(InputStream inputStream, OutputStream outputStream) throws Exception {
int c = 0;
int state = 0;
StringBuilder sb = new StringBuilder();
Set<String> matches = new HashSet<>();
Map<String, String> refMap = new LinkedHashMap<>();
Map<String, String> styles = new HashMap<>();
String prop = null;
checkForMatch("@before@", matches, refMap);
while (c != -1) {
c = inputStream.read();
if (c == -1) {
state = -1;
} else {
outputStream.write(c);
}
switch (state) {
case -1: // Process template
for (String match : matches) {
String template = refMap.get(match);
if (!template.contains(DELIM)) {
continue;
}
for (Entry<String, String> entry : styles.entrySet()) {
String replace = DELIM + entry.getKey() + DELIM;
template = template.replace(replace, entry.getValue());
}
refMap.put(match, template);
}
matches.clear();
styles.clear();
if (c == -1) {
continue;
}
state = 0;
// Fall through intended
case 0: // Baseline
switch (c) {
case '/': // Possible comment start
state = 1;
break;
case '<': // Directive start
state = 10;
break;
case '{': // Declaration block
state = 20;
break;
case ',': // Selector separator
break;
case '}': // Don't know why these occur, but ignore them.
continue;
default:
sb.append((char) c);
continue;
}
checkForMatch(sb.toString(), matches, refMap);
sb.setLength(0);
break;
case 1: // Possible comment
state = c == '*' ? 2 : 0;
break;
case 2: // Possible comment end
state = c == '*' ? 3 : state;
break;
case 3: // Comment end
state = c == '/' ? 0 : c == '*' ? state : 2;
break;
case 10: // Directive end
state = c == '>' ? 0 : state;
break;
case 20: // Declaration block
switch (c) {
case '}': // End block
state = -1;
break;
case ':': // Start of property value
prop = sb.toString().trim();
sb.setLength(0);
state = 30;
break;
default: // Build property name
sb.append((char) c);
break;
}
break;
case 30: // Property value
switch (c) {
case ';': // Property separator
case '}': // Block terminator
styles.put(prop, sb.toString());
sb.setLength(0);
state = c == ';' ? 20 : -1;
break;
default: // Build property value
sb.append((char) c);
break;
}
break;
}
}
checkForMatch("@after@", matches, defMap);
writeMap(refMap, outputStream);
writeMap(defMap, outputStream);
if (!srcMap.isEmpty()) {
mojo.getLog().warn("The following entries failed to match and were ignored:");
for (Entry<String, String> entry : srcMap.entrySet()) {
mojo.getLog().warn(" " + entry);
}
}
}
}