001: /*
002: ItsNat Java Web Application Framework
003: Copyright (C) 2007 Innowhere Software Services S.L., Spanish Company
004: Author: Jose Maria Arranz Santamaria
005:
006: This program is free software: you can redistribute it and/or modify
007: it under the terms of the GNU Affero General Public License as published by
008: the Free Software Foundation, either version 3 of the License, or
009: (at your option) any later version. See the GNU Affero General Public
010: License for more details. See the copy of the GNU Affero General Public License
011: included in this program. If not, see <http://www.gnu.org/licenses/>.
012: */
013:
014: package org.itsnat.impl.core;
015:
016: import java.util.HashMap;
017: import java.util.Iterator;
018: import java.util.LinkedList;
019: import java.util.Map;
020: import org.itsnat.core.ItsNatDOMException;
021: import org.itsnat.core.domutil.ItsNatDOMUtil;
022: import org.itsnat.impl.core.dom.render.DOMRenderImpl;
023: import org.w3c.dom.Attr;
024: import org.w3c.dom.Document;
025: import org.w3c.dom.DocumentFragment;
026: import org.w3c.dom.Element;
027: import org.w3c.dom.Node;
028: import org.xml.sax.InputSource;
029: import org.w3c.dom.CharacterData;
030: import org.w3c.dom.NamedNodeMap;
031: import org.w3c.dom.Text;
032:
033: /**
034: *
035: * @author jmarranz
036: */
037: public abstract class MarkupTemplateVersionImpl extends
038: MarkupContainerImpl {
039: public static final String ITSNAT_NAMESPACE = "http://itsnat.org/itsnat";
040: protected MarkupTemplateImpl markupTemplate;
041: protected Document templateDoc;
042: protected long timeStamp;
043: protected DOMRenderImpl nodeRender; // Sirve para serializar nodos concretos no el documento completo
044: protected MarkupTemplateVersionDelegateImpl templateDelegate;
045: protected Map elementCacheMap;
046: protected LinkedList fragments = new LinkedList(); // Se recorrerá en multihilo no podemos ahorrar memoria con creación demorada
047:
048: /**
049: * Creates a new instance of MarkupTemplateVersionImpl
050: */
051: public MarkupTemplateVersionImpl(MarkupTemplateImpl markupDesc,
052: long timeStamp) {
053: this .id = markupDesc.getItsNatServletImpl().generateUniqueId();
054: this .markupTemplate = markupDesc;
055: this .timeStamp = timeStamp;
056:
057: this .templateDelegate = createMarkupTemplateVersionDelegate();
058: this .templateDoc = parseDocument(markupDesc.getInputSource());
059: this .nodeRender = DOMRenderImpl.getDOMRender(templateDoc,
060: getMIME(), getEncoding(), true);
061:
062: // Resolvemos lo primero los includes, así permitimos que pueda cachearse su contenido
063: // Hay que tener en cuenta que al incluir un fragmento los nodos pueden estar cacheados
064: // con el sistema de cacheado de los fragmentos incluidos con <include>, que es diferente al del documento
065: // por tanto puede haber un doble cacheado (que no tienen por qué interferir)
066: Element rootElem = templateDoc.getDocumentElement();
067: processCommentsIncludesInTree(rootElem);
068: }
069:
070: public boolean isOnLoadCacheStaticNodes() {
071: return markupTemplate.isOnLoadCacheStaticNodes();
072: }
073:
074: protected abstract void doCacheDocument();
075:
076: public boolean isUniqueIdGeneratorSync() {
077: return true;
078: }
079:
080: public boolean isHTMLorXHTML() {
081: return markupTemplate.isHTMLorXHTML();
082: }
083:
084: public boolean isXHTML() {
085: return markupTemplate.isXHTML(); // No cambia
086: }
087:
088: public boolean isHTML() {
089: return markupTemplate.isHTML(); // No cambia
090: }
091:
092: public String getId() {
093: return id;
094: }
095:
096: public Document getDocument() {
097: return templateDoc;
098: }
099:
100: public DOMRenderImpl getNodeDOMRender() {
101: return nodeRender;
102: }
103:
104: public String getEncoding() {
105: return markupTemplate.getEncoding();
106: }
107:
108: public String getMIME() {
109: // El MIME en el template no puede cambiar, no hay problema de versiones
110: return markupTemplate.getMIME();
111: }
112:
113: public boolean isInvalid(long newTimeStamp) {
114: boolean invalid = (newTimeStamp > this .timeStamp);
115: if (invalid)
116: return true;
117:
118: // Seguimos con los fragmentos incluidos, si alguno ha sido cambiado en el disco duro el template padre es inválido y necesita recargarse
119:
120: for (Iterator it = fragments.iterator(); it.hasNext();) {
121: DocFragmentTemplateVersionImpl docFragTemplateIncVersion = (DocFragmentTemplateVersionImpl) it
122: .next();
123: if (docFragTemplateIncVersion.isInvalid())
124: return true;
125: }
126:
127: return false; // Nada ha cambiado
128: }
129:
130: public boolean isInvalid() {
131: long newTimeStamp = markupTemplate.getCurrentTimeStamp();
132: return isInvalid(newTimeStamp);
133: }
134:
135: public boolean processCommentsIncludesInNode(Node node) {
136: if (node.getNodeType() != Node.ELEMENT_NODE)
137: return false;
138:
139: Element nodeElem = (Element) node;
140: String namespace = nodeElem.getNamespaceURI();
141: if ((namespace != null) && ITSNAT_NAMESPACE.equals(namespace)) {
142: String localName = nodeElem.getLocalName();
143:
144: if (localName.equals("include")) {
145: String fragName = nodeElem.getAttribute("name");
146: processIncludeReplacingNode(nodeElem, fragName);
147: return true;
148: } else if (localName.equals("comment")) {
149: nodeElem.getParentNode().removeChild(nodeElem);
150:
151: return true;
152: } else
153: throw new ItsNatDOMException("Unknown itsnat tag name:"
154: + localName, nodeElem);
155: } else if (nodeElem.hasAttributes()) {
156: NamedNodeMap attribs = nodeElem.getAttributes();
157: for (int i = 0; i < attribs.getLength(); i++) {
158: Attr attr = (Attr) attribs.item(i);
159: namespace = attr.getNamespaceURI();
160: if ((namespace != null)
161: && ITSNAT_NAMESPACE.equals(namespace)) {
162: String localName = attr.getLocalName();
163: if (localName.equals("include")) {
164: String fragName = attr.getValue();
165: processIncludeReplacingNode(nodeElem, fragName);
166: nodeElem.removeAttributeNode(attr);
167: } else if (localName.equals("includeInside")) {
168: String fragName = attr.getValue();
169: processIncludeInsideNode(nodeElem, fragName);
170: nodeElem.removeAttributeNode(attr);
171: } else if (localName.equals("comment")) {
172: nodeElem.removeAttributeNode(attr);
173: }
174: // Pueden ser otros atributos con prefijo itsnat:
175: }
176: }
177: }
178:
179: return false; // No es ni include ni comment
180: }
181:
182: public DocumentFragment loadDocumentFragment(Element includeElem,
183: String fragName) {
184: DocFragmentTemplateImpl docFragTemplateInc = (DocFragmentTemplateImpl) markupTemplate
185: .getItsNatServlet().getDocFragmentTemplate(fragName);
186: if (docFragTemplateInc == null)
187: throw new ItsNatDOMException(
188: "Document fragment not found: " + fragName,
189: includeElem);
190:
191: DocFragmentTemplateVersionImpl docFragTemplateIncVersion = docFragTemplateInc
192: .getDocFragmentTemplateVersion();
193: fragments.add(docFragTemplateIncVersion);
194:
195: return templateDelegate.loadDocumentFragmentIncluded(
196: docFragTemplateIncVersion, includeElem);
197: }
198:
199: public void processIncludeReplacingNode(Element includeElem,
200: String fragName) {
201: DocumentFragment docFrag = loadDocumentFragment(includeElem,
202: fragName);
203: Node parent = includeElem.getParentNode();
204: parent.insertBefore(docFrag, includeElem);
205: parent.removeChild(includeElem);
206: }
207:
208: public void processIncludeInsideNode(Element includeElem,
209: String fragName) {
210: DocumentFragment docFrag = loadDocumentFragment(includeElem,
211: fragName);
212:
213: ItsNatDOMUtil.removeAllChildren(includeElem); // Por si acaso no está vacío
214: includeElem.appendChild(docFrag); // El DocumentFragment queda vacío (creo)
215: }
216:
217: public void processCommentsIncludesInTree(Node node) {
218: if (!processCommentsIncludesInNode(node))
219: processCommentsIncludesInChildren(node);
220: }
221:
222: public void processCommentsIncludesInChildren(Node parent) {
223: Node child = parent.getFirstChild();
224: while (child != null) {
225: Node nextChild = child.getNextSibling(); // Antes de que pueda ser removido/reemplazado
226:
227: processCommentsIncludesInTree(child);
228:
229: child = nextChild;
230: }
231: }
232:
233: private String serializeChildNodes(Element parent) {
234: StringBuffer code = new StringBuffer();
235: Node child = parent.getFirstChild();
236: while (child != null) {
237: code.append(serializeNode(child));
238:
239: child = child.getNextSibling();
240: }
241: return code.toString();
242: }
243:
244: protected String serializeNode(Node node) {
245: return templateDelegate.serializeNode(node);
246: }
247:
248: protected boolean isNodeCacheable(Node node,
249: LinkedList cacheableChildrenGlobal) {
250: if (node instanceof Element)
251: return isElementCacheable((Element) node,
252: cacheableChildrenGlobal);
253: else if (node instanceof CharacterData)
254: return isCharacterDataCacheable((CharacterData) node);
255: else
256: return false;
257: // Otros tipos (incluido el objeto Document), no sabemos como cachearlos
258: // o no tiene sentido (caso Document,DocumentFragment) y podrían tener variables que no consideramos analizar,
259: // nos curamos en salud, evitarán que los nodos superiores sean cacheables
260: // pero hay que tener en cuenta que son nodos raros
261: // en el contenido de un documento
262: }
263:
264: private boolean isCharacterDataCacheable(CharacterData node) {
265: // Nodos de texto, comentarios etc.
266: // El objetivo en este caso no es reducir el número de objetos DOM
267: // pues el Text etc se mantendrá, pero en los documentos clonados
268: // no aparecerá todo el rollo de texto estático repetidas veces.
269: // Es cacheable si no contiene variables
270: return !hasVariables(node.getData());
271: }
272:
273: protected boolean isElementCacheable(Element elem,
274: LinkedList cacheableChildrenGlobal) {
275: // Es cacheable si el contenido (los hijos) no van a cambiar, es decir si son estáticos
276:
277: if (isDeclaredNotCacheable(elem))
278: return false; // Ni el elemento ni los hijos son cacheables
279: if (declaredAsComponent(elem))
280: return false; // Los componentes por su naturaleza modifican los nodos contenidos, por tanto ni el elemento ni los hijos son cacheables
281:
282: boolean elementCacheable = !isElementNotCacheableByNamespace(elem);
283: // Si false el elemento no es cacheable pero podrían serlo los hijos, es el caso de que el elemento no pueda ser usado con innerHTML pero los elementos hijo sí
284:
285: // Los atributos aunque no sean estáticos no importa
286: // pues es el contenido del nodo es el que se cachea realmente
287:
288: // Aunque sea elementCacheable = true los hijos puede ser que sean cacheables o todos o algunos
289:
290: boolean childrenAreStatic = areChildElementStatic(elem,
291: cacheableChildrenGlobal);
292:
293: return elementCacheable && childrenAreStatic;
294: }
295:
296: protected boolean areChildElementStatic(Element elem,
297: LinkedList cacheableChildrenGlobal) {
298: // Un elemento puede cachearse si sus hijos son estáticos (salvo que haya otra razón)
299:
300: if (!elem.hasChildNodes())
301: return true; // El elemento es cacheable desde el pto. de vista de los hijos (no tiene), otra cosa es que no merezca la pena pues el contenido es vacío, es importante devolver true y no false pues si devolvemos false hacemos creer que todo el árbol superior NO puede ser cacheable
302:
303: LinkedList cacheableChildrenLocal = new LinkedList();
304: boolean allChildrenCacheable = true;
305: Node child = elem.getFirstChild();
306: while (child != null) {
307: if (!isNodeCacheable(child, cacheableChildrenLocal)) {
308: allChildrenCacheable = false;
309: } else {
310: // Si no son todos los hijos cacheables por alguna razón,
311: // entonces no podemos cachear el nodo padre (elemento o documento)
312: // Al menos cacheamos los nodos hijos cacheables
313:
314: cacheableChildrenLocal.add(child);
315: }
316:
317: child = child.getNextSibling();
318: }
319:
320: if (!allChildrenCacheable) {
321: // Si no son todos los hijos cacheables por alguna razón,
322: // entonces no podemos cachear todo el nodo (elemento o documento)
323: // Al menos cacheamos los nodos hijos cacheables
324: if (!cacheableChildrenLocal.isEmpty()) {
325: cacheableChildrenGlobal.addAll(cacheableChildrenLocal);
326: }
327:
328: return false;
329: } else {
330: // El que todos los nodos hijo sean cacheables no implica que sean estáticos pues
331: // pueden tener atributos no estáticos, si tienen atributos
332: // no estáticos no podemos cachear el nodo padre.
333: // Sólo si los hijos son totalmente estáticos (contenido cacheable + atributos estáticos)
334: // podemos cachear el nodo
335: // Sabemos que todos los nodos al menos son cacheables
336: boolean allChildrenStatic = true;
337: child = elem.getFirstChild();
338: while (child != null) {
339: if (!areAttributesStatic(child)) {
340: allChildrenStatic = false;
341: break;
342: }
343:
344: child = child.getNextSibling();
345: }
346:
347: if (!allChildrenStatic && !cacheableChildrenLocal.isEmpty()) {
348: // No son estáticos todos, pero al menos cacheamos los hijos
349: cacheableChildrenGlobal.addAll(cacheableChildrenLocal);
350: }
351:
352: return allChildrenStatic;
353: }
354: }
355:
356: private boolean isDeclaredNotCacheable(Element elem) {
357: // atributo itsnat:nocache
358: String nocache = elem.getAttributeNS(ITSNAT_NAMESPACE,
359: "nocache");
360: return "true".equals(nocache);
361: }
362:
363: private boolean areAttributesStatic(Node node) {
364: if (!node.hasAttributes())
365: return true;
366:
367: boolean allStatic = true;
368: NamedNodeMap attributes = node.getAttributes();
369: for (int i = 0; i < attributes.getLength(); i++) {
370: Attr attr = (Attr) attributes.item(i);
371: if (hasVariables(attr.getValue())) {
372: allStatic = false;
373: break;
374: }
375: }
376:
377: return allStatic;
378: }
379:
380: private boolean hasVariables(String content) {
381: int index = content.indexOf("${");
382: return index != -1;
383: }
384:
385: public Document parseDocument(InputSource input) {
386: return templateDelegate.parseDocument(input, getEncoding());
387: }
388:
389: public boolean declaredAsComponent(Element elem) {
390: return templateDelegate.declaredAsComponent(elem);
391: }
392:
393: public boolean isElementNotCacheableByNamespace(Element elem) {
394: return templateDelegate.isElementNotCacheableByNamespace(elem);
395: }
396:
397: protected void inspectNodeToCache(Node node) {
398: LinkedList cacheableChildrenGlobal = new LinkedList();
399: boolean isCacheable = isNodeCacheable(node,
400: cacheableChildrenGlobal);
401: if (isCacheable)
402: addNodeToCache(node);
403: else {
404: for (Iterator it = cacheableChildrenGlobal.iterator(); it
405: .hasNext();) {
406: Node child = (Node) it.next();
407: addNodeToCache(child);
408: }
409: }
410: }
411:
412: protected void addNodeToCache(Node node) {
413: if (node instanceof Element)
414: addElementToCache((Element) node);
415: else if (node instanceof CharacterData)
416: addCharacterDataToCache((CharacterData) node);
417: else
418: throw new ItsNatDOMException("INTERNAL ERROR", node);
419: }
420:
421: protected void addElementToCache(Element elem) {
422: // No vale la pena obviamente cachear elementos sin hijos
423: if (!elem.hasChildNodes())
424: return;
425:
426: Node firstChild = elem.getFirstChild();
427:
428: if ((firstChild.getNextSibling() == null)
429: && (firstChild instanceof CharacterData)) {
430: // Elemento con un único nodo hijo y de texto
431: addCharacterDataToCache((CharacterData) firstChild);
432: } else {
433: String code = serializeChildNodes(elem); // No se expanden los nodos cacheados de fragmentos incluidos con nodos <include>, ya se hará cuando se envíe al cliente
434:
435: ItsNatDOMUtil.removeAllChildren(elem);
436:
437: Document doc = elem.getOwnerDocument();
438: String markedCode = addToCache(code);
439: Text markNode = doc.createTextNode(markedCode);
440: elem.appendChild(markNode);
441: }
442: }
443:
444: protected Map getElementCacheMap() {
445: if (elementCacheMap == null)
446: this .elementCacheMap = new HashMap(); // Así ahorramos memoria si el cache está desactivado
447: return elementCacheMap;
448: }
449:
450: public boolean hasCachedNodes() {
451: return (elementCacheMap != null) || (usedTemplates != null);
452: }
453:
454: protected void addCharacterDataToCache(CharacterData node) {
455: // El objetivo en este caso no es reducir el número de objetos DOM
456: // pues el Text etc se mantendrá, pero en los documentos clonados
457: // no aparecerá todo el rollo de texto estático repetidas veces.
458:
459: String value = node.getData();
460: if (value.length() < 100)
461: return; // Menos de 100 letras no merece la pena, así excluimos nodos de texto cortos, separadores etc, pues la ganancia de cachear nodos de texto es únicamente para disminuir el tamaño de los documentos clonados y el "descacheado" lleva su tiempo
462:
463: // Aunque estemos en la serialización del proceso de cacheo
464: // puede haber nodos cacheados de fragmentos incluidos con nodos <include>
465: // no expandimos dichos nodos cacheados, ya
466: // se hará cuando se envíe el código al cliente
467:
468: String code = serializeNode(node);
469: int nodeType = node.getNodeType();
470: if ((nodeType == Node.COMMENT_NODE)
471: || (nodeType == Node.CDATA_SECTION_NODE)) {
472: String prefix = null;
473: String suffix = null;
474: if (nodeType == Node.COMMENT_NODE) {
475: prefix = "<!--";
476: suffix = "-->";
477: } else {
478: prefix = "<![CDATA[";
479: suffix = "]]>";
480: }
481:
482: // Quitamos el prefijo y sufijo pues nos interesa guardar el contenido
483: // pues al serializar de nuevo el nodo al enviar al cliente se añadirá
484: // de nuevo el prefijo/sufijo
485: code = removePrefixSuffix(code, prefix, suffix);
486: } else if ((nodeType == Node.TEXT_NODE) && isXHTML()) {
487: // El serializador XHTML engloba el texto dentro de un <![CDATA[ ... ]]>
488: // pero como substituimos el texto por la marca al serializar el nodo finalmente
489: // de nuevo se añadirá <![CDATA[ ... ]]> existiendo por tanto dos erróneamente.
490: // Por tanto quitamos los de ahora
491:
492: String prefix = "<![CDATA[";
493: String suffix = "]]>";
494: code = removePrefixSuffix(code, prefix, suffix);
495: }
496:
497: String markCode = addToCache(code);
498: node.setData(markCode);
499: }
500:
501: public static String removePrefixSuffix(String code, String prefix,
502: String suffix) {
503: code = code.substring(prefix.length());
504: code = code.substring(0, code.length() - suffix.length());
505: return code;
506: }
507:
508: protected String addToCache(String code) {
509: CachedNode cachedNode = new CachedNode(this , code);
510: Map elementCacheMap = getElementCacheMap();
511: elementCacheMap.put(cachedNode.getId(), cachedNode);
512: return cachedNode.getMarkCode();
513: }
514:
515: public MarkupTemplateVersionImpl getUsedMarkupTemplateVersion(
516: String id) {
517: if (getId().equals(id))
518: return this ;
519: else
520: return super .getUsedMarkupTemplateVersion(id);
521: }
522:
523: protected abstract MarkupTemplateVersionDelegateImpl createMarkupTemplateVersionDelegate();
524: }
|