/** Copyright (C) 2012 Delcyon, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.delcyon.capo.xml; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.HashMap; import java.util.Set; import java.util.Stack; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.xml.sax.SAXException; import com.delcyon.capo.CapoApplication; import com.delcyon.capo.ContextThread; import com.delcyon.capo.Configuration.PREFERENCE; import com.delcyon.capo.datastream.StreamProcessor; import com.delcyon.capo.datastream.StreamProcessorProvider; import com.delcyon.capo.datastream.StreamUtil; import com.delcyon.capo.xml.cdom.CDocument; /** * @author jeremiah */ @SuppressWarnings("unchecked") @StreamProcessorProvider(streamIdentifierPatterns = { "<\\?xml .*"}) public class XMLStreamProcessor implements StreamProcessor { private static HashMap<String, Class<? extends XMLProcessor>> xmlProcessorHashMap = new HashMap<String, Class<? extends XMLProcessor>>(); static { Set<String> xmlProcessorProviderSet = CapoApplication.getAnnotationMap().get(XMLProcessorProvider.class.getCanonicalName()); for (String className : xmlProcessorProviderSet) { try { Class<? extends XMLProcessor> xmlRequestProcessorClass = (Class<? extends XMLProcessor>) Class.forName(className); XMLProcessorProvider streamConsumer = xmlRequestProcessorClass.getAnnotation(XMLProcessorProvider.class); String[] documentElementNames = streamConsumer.documentElementNames(); //TODO make this namespace aware String[] namespaces = streamConsumer.namespaceURIs(); for (String documentElementName : documentElementNames) { xmlProcessorHashMap.put(documentElementName, xmlRequestProcessorClass); CapoApplication.logger.log(Level.CONFIG, "Loaded XMLProcessor '"+documentElementName+"' from "+xmlRequestProcessorClass.getSimpleName()); } } catch (Exception e) { CapoApplication.logger.log(Level.WARNING, "Couldn't load "+className+" as an XMLProcessor", e); } } } public static XMLProcessor getXMLProcessor(String xmlProcessorName) throws Exception { Class<? extends XMLProcessor> xmlProcessorClass = xmlProcessorHashMap.get(xmlProcessorName); if (xmlProcessorClass != null) { return xmlProcessorClass.newInstance(); } else { return null; } } private Transformer transformer; private DocumentBuilder documentBuilder; private BufferedInputStream inputStream; private OutputStream outputStream; private HashMap<String, String> sessionHashMap; //private ThreadGroup subThreadGroup = null; private Exception exception = null; private Document returnDocument; private long initialTID = -1l; private Thread processorThread = Thread.currentThread(); private long processorTID = processorThread.getId(); // private byte[] ackBuffer = new byte[]{-1}; private ReentrantLock lock = new ReentrantLock(true); private Condition readyToRead = lock.newCondition(); private Condition readyToACK = lock.newCondition(); public XMLStreamProcessor() throws Exception { _init(); } public XMLStreamProcessor(BufferedInputStream bufferedInputStream, OutputStream outputStream) throws Exception { this.inputStream = bufferedInputStream; this.outputStream = outputStream; _init(); } /** * This is the internal initialization method, the API requires an init method, * but we don't want to always have to call it as we use this class as a utility class as well * @throws Exception */ private void _init() throws Exception { TransformerFactory tFactory = TransformerFactory.newInstance(); transformer = tFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); documentBuilder = documentBuilderFactory.newDocumentBuilder(); } /** * does nothing, just here for API * TODO make sure we actually need this for the API, could just be a hold over from refactoring */ @Override public void init(HashMap<String, String> sessionHashMap) throws Exception { this.sessionHashMap = sessionHashMap; } @Override public void processStream(BufferedInputStream bufferedInputStream, OutputStream outputStream) throws Exception { //get the thread variables all setup. We don't want to do this in init because we're not in our own thread until we start processing. HashMap<String, ContextThread> threadMap = new HashMap<String, ContextThread>(); //subThreadGroup = new ThreadGroup(getClass().getName()+" - "+System.nanoTime()); //indicate we're actually in the read loop // ackBuffer[0] = 0; //setup our data streams this.inputStream = bufferedInputStream; this.outputStream = outputStream; while(true) { //check to see if a thread has thrown an exception and if so re-throw it, and exit out. if(exception != null) { throw exception; } //read the document stream Document document = getDocument(bufferedInputStream); //check for end of stream if(document == null) { break; } //check to see if we got an empty document, because we're actually dealing with an ACK from the remote end after a document write if(document.getDocumentElement() == null) { continue; } //load client request String documentElementName = document.getDocumentElement().getLocalName(); //System.err.println("searching for "+documentElementName); String tid = document.getDocumentElement().getAttribute("TID"); //TODO tid null check XMLProcessor xmlProcessor = getXMLProcessor(documentElementName); if (xmlProcessor != null) { xmlProcessor.init(document, this, outputStream,sessionHashMap); ContextThread contextThread = new ContextThread(processorThread.getThreadGroup(), xmlProcessor); if(Thread.currentThread() instanceof ContextThread) { contextThread.setSession(((ContextThread) Thread.currentThread()).getSession()); } threadMap.put(tid, contextThread); if(initialTID < 0l) { initialTID = contextThread.getId(); } //don't start until we can make sure that nothing is trying to write // synchronized (ackBuffer) { //check to see if this xmlProcessor handles it's own streams. if(xmlProcessor.isStreamProcessor() == true) { //if so, don't make a separate thread, just run it in this one. contextThread.run(); if(exception != null) { throw exception; } break; } else { contextThread.start(); } } continue; } //didn't find a match, so didn't make a new thread, check to see if there is something waiting for us. lock.lock(); if(lock.hasWaiters(readyToRead) == true) { this.returnDocument = document; readyToRead.signal(); lock.unlock(); } //no one found waiting, so let's poll for a bit, then give up if nothing ever shows up. else { //wait for the stack to fill up for a sec, just to double check. int attemptCount = 0; while(lock.hasWaiters(readyToRead) == false && attemptCount < 30) { lock.unlock(); Thread.sleep(500); lock.lock(); } if(lock.hasWaiters(readyToRead) == true) { this.returnDocument = document; readyToRead.signal(); lock.unlock(); } else { //well, we tried CapoApplication.logger.log(Level.SEVERE, "Unknown XML Type: "+documentElementName); lock.unlock(); throw new Exception("Unknown XML Type: "+documentElementName); } } } } /** * Sends a document over the outputStream terminated by a char = '0' * @param document * @throws Exception */ public void writeDocument(Document document) throws Exception { int writeResponseValue = 1;//TODO-1; //synchronized (ackBuffer) { long tid = Thread.currentThread().getId(); //make sure we always use a know TID, //our initial TID will be wrong because the first write doesn't come from the thread, but the initial call to xmlStrema Processor if(tid == initialTID) { tid = processorTID; } document.getDocumentElement().setAttribute("TID",tid+""); //pause the reading thread, until we get our OK, or Error back from the client, since we're taking over the inputStream for a second transformer.transform(new DOMSource(document), new StreamResult(outputStream)); if (CapoApplication.logger.isLoggable(Level.FINER)) { CapoApplication.logger.log(Level.FINER, "Wrote Document:"); XPath.dumpNode(document, System.out); } outputStream.write(0); outputStream.flush(); CapoApplication.logger.log(Level.FINE, "SENT END bit to Remote After WRITE: 0"); boolean notInReadLoop = false; //check to see if we have our return value yet // if(ackBuffer[0] == 0) // { // ackBuffer.wait(30000); // } // //check to see if we're actually in the read loop here // else if(ackBuffer[0] == -1) // { // notInReadLoop = true; // //since we're not in the loop, we're going to have to read the stream ourselves // byte[] buffer = new byte[2]; // StreamUtil.fullyReadIntoBufferUntilPattern(inputStream,buffer, (byte)0); // if(buffer[1] == 0) // { // ackBuffer[0] = buffer[0]; // } // else // { // throw new Exception("Expecting an ACK, not "+buffer); // } // } // writeResponseValue = ackBuffer[0]; // //reset the buffer to 0 // if(notInReadLoop == false) // { // ackBuffer[0] = 0; // } // else // { // ackBuffer[0] = -1; // } // ackBuffer.notify(); } CapoApplication.logger.log(Level.FINE, "READ OK bit to Remote After WRITE: "+writeResponseValue); if(writeResponseValue != 1) { XPath.dumpNode(document, System.err); throw new Exception("Remote End Reported an Error: "+writeResponseValue); } } /** * gets the next document from the inputStream * @return response Document * @throws Exception */ public Document readNextDocument() throws Exception { //don't lock our stream processing thread if(Thread.currentThread().getId() == processorTID) { return getDocument(inputStream); } else { lock.lock(); //this unlocks until we are signaled, at which point we re aquire the lock readyToRead.await(30,TimeUnit.SECONDS); lock.unlock(); //so we must unlock it once we are done. return returnDocument; } } /** * Reads an input stream until and EOF or '0x0' char is found, and tries to parse the results * @param inputStream * @return * @throws Exception */ private Document getDocument(BufferedInputStream inputStream) throws Exception { inputStream.mark(CapoApplication.getConfiguration().getIntValue(PREFERENCE.BUFFER_SIZE)); byte[] buffer = StreamUtil.fullyReadUntilPattern(inputStream,false, (byte)0); //end of stream reached if(buffer == null) { return null; } //check for write ack message // if(buffer.length == 1) // { // synchronized(ackBuffer) // { // //set the value // ackBuffer[0] = buffer[0]; // //let anyone listening know that we've processed something // ackBuffer.notify(); // //wait until they tell us to keep processing // ackBuffer.wait(); // } // //return blank document to indicate this was just an ACK // return new CDocument(); // } // else if(buffer.length >= 10 && new String(buffer,0,9).startsWith("FINISHED:")) { inputStream.reset(); return null; } else { try { Document readDocument = documentBuilder.parse(new ByteArrayInputStream(buffer)); if (CapoApplication.logger.isLoggable(Level.FINER)) { CapoApplication.logger.log(Level.FINER, "Read Document:"); XPath.dumpNode(readDocument, System.out); } //send ok to sender // outputStream.write(1); // outputStream.write(0); // outputStream.flush(); // CapoApplication.logger.log(Level.FINE, "SENT OK bit to Remote After READ: 1,0"); return readDocument; } catch (SAXException saxException) { // CapoApplication.logger.log(Level.WARNING,"length = "+buffer.length+" buffer = ["+new String(buffer)+"]\nRAW:["+Arrays.toString(buffer)+"]"); // outputStream.write(2); // outputStream.write(0); // outputStream.flush(); throw saxException; } } } public BufferedInputStream getInputStream() { return inputStream; } public void throwException(Exception exception) { this.exception = exception; } }