blob: 4c0cc8720ac434890d69aa2140d130f6114dd763 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2009 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jst.jsp.core.internal.document;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jst.jsp.core.internal.Logger;
import org.eclipse.jst.jsp.core.internal.contentproperties.JSPFContentProperties;
import org.eclipse.jst.jsp.core.internal.modelhandler.EmbeddedTypeStateData;
import org.eclipse.jst.jsp.core.internal.provisional.contenttype.ContentTypeIdForJSP;
import org.eclipse.jst.jsp.core.internal.provisional.contenttype.IContentDescriptionForJSP;
import org.eclipse.jst.jsp.core.internal.text.StructuredTextPartitionerForJSP;
import org.eclipse.wst.html.core.internal.provisional.contenttype.ContentTypeFamilyForHTML;
import org.eclipse.wst.sse.core.internal.document.DocumentReader;
import org.eclipse.wst.sse.core.internal.ltk.modelhandler.EmbeddedTypeHandler;
import org.eclipse.wst.sse.core.internal.modelhandler.EmbeddedTypeRegistry;
import org.eclipse.wst.sse.core.internal.modelhandler.EmbeddedTypeRegistryImpl;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapter;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapterFactory;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredPartitioning;
import org.eclipse.wst.sse.core.internal.util.Debug;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import com.ibm.icu.util.StringTokenizer;
/**
* This class has the responsibility to provide an embedded factory registry
* for JSP Aware INodeAdapter Factories to use.
*
* Typically, the embedded type is to be considered a feature of the document,
* so JSP Aware AdpaterFactories should call
* getAdapter(PageDirectiveAdapter.class) directoy on the document (or owning
* document) node.
*/
public class PageDirectiveAdapterImpl implements PageDirectiveAdapter {
protected static final String STR_CHARSET = "charset"; //$NON-NLS-1$
private final static Object adapterType = PageDirectiveAdapter.class;
private IStructuredModel model;
protected final String[] JAVASCRIPT_LANGUAGE_KEYS = new String[]{"javascript", "javascript1.0", "javascript1.1_3", "javascript1.2", "javascript1.3", "javascript1.4", "javascript1.5", "javascript1.6", "jscript", "sashscript"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$
protected final String[] JAVA_LANGUAGE_KEYS = new String[]{"java"}; //$NON-NLS-1$
/**
* Constructor for PageDirectiveAdapterImpl.
*/
public PageDirectiveAdapterImpl(INodeNotifier target) {
super();
notifierAtCreation = target;
// we need to remember our instance of model,
// in case we need to "signal" a re-init needed.
if (target instanceof IDOMNode) {
IDOMNode node = (IDOMNode) target;
model = node.getModel();
}
}
/**
* parses the full contentType value into its two parts the contentType,
* and the charset, if present. Note: this method is a lightly modified
* version of a method in AbstractHeadParser. There, we're mostly
* interested in the charset part of contentTypeValue. Here, we're mostly
* interested in the mimeType part.
*/
private String getMimeTypeFromContentTypeValue(String contentTypeValue) {
if (contentTypeValue == null)
return null;
String cleanContentTypeValue = StringUtils.stripNonLetterDigits(contentTypeValue);
StringTokenizer tokenizer = new StringTokenizer(cleanContentTypeValue, ";= \t\n\r\f"); //$NON-NLS-1$
int tLen = tokenizer.countTokens();
// if contains encoding should have three tokens, the mimetype, the
// word 'charset', and the encoding value
String[] tokens = new String[tLen];
int j = 0;
while (tokenizer.hasMoreTokens()) {
tokens[j] = tokenizer.nextToken();
j++;
}
//
// Following is the common form for target expression
// <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
// But apparrently is also valid without the content type there,
// just the charset, as follows:
// <META http-equiv="Content-Type" content="charset=UTF-8">
// So we'll loop through tokens and key off of 'charset'
int charsetPos = -1;
for (int i = 0; i < tokens.length; i++) {
if (tokens[i].equalsIgnoreCase(STR_CHARSET)) {
charsetPos = i;
break;
}
}
// String charset = null;
String contentType = null;
if (charsetPos > -1) {
// case where charset was present
// int charsetValuePos = charsetPos + 1;
// if (charsetValuePos < tokens.length) {
// charset = tokens[charsetValuePos];
// }
int contentTypeValuePos = charsetPos - 1;
if (contentTypeValuePos > -1) {
contentType = tokens[contentTypeValuePos];
}
}
else {
// charset was not present, so if there's
// a value, we assume its the contentType value
if (tokens.length > 0) {
contentType = tokens[0];
}
}
return contentType;
}
private EmbeddedTypeHandler embeddedTypeHandler;
private List embeddedFactoryRegistry = new ArrayList();
private String cachedLanguage;
private String cachedContentType;
private INodeNotifier notifierAtCreation;
private String elIgnored = null;
private int firstLanguagePosition = -1;
private int firstContentTypePosition = -1;
/*
* @see INodeAdapter#isAdapterForType(Object)
*/
public boolean isAdapterForType(Object type) {
return (type == adapterType);
}
/*
* @see INodeAdapter#notifyChanged(INodeNotifier, int, Object, Object,
* Object, int)
*/
public void notifyChanged(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
}
public void setEmbeddedType(EmbeddedTypeHandler handler) {
// if really the same handler, no need for further processing
if (embeddedTypeHandler == handler) {
return;
}
// then one exists, and the new one is truely different, so we need to
// release and remove current factories
if (embeddedTypeHandler != null) {
Iterator list = embeddedFactoryRegistry.iterator();
while (list.hasNext()) {
INodeAdapterFactory factory = (INodeAdapterFactory) list.next();
factory.release();
}
embeddedFactoryRegistry.clear();
}
embeddedTypeHandler = handler;
// when the handler is set, "transfer" its factories to our own list.
// note: our own list may also be added to else where, such as on
// "editor side".
if (embeddedTypeHandler != null) {
Iterator iterator = embeddedTypeHandler.getAdapterFactories().iterator();
while (iterator.hasNext()) {
INodeAdapterFactory factory = (INodeAdapterFactory) iterator.next();
embeddedFactoryRegistry.add(factory);
}
}
}
/**
* @see PageDirectiveAdapter#adapt(INodeNotifier, Object)
*/
public INodeAdapter adapt(INodeNotifier notifier, Object type) {
INodeAdapter result = null;
// if embeddedContentType hasn't been set,
// then we can not adapt it.
if (embeddedTypeHandler != null) {
if (embeddedFactoryRegistry != null) {
Iterator iterator = embeddedFactoryRegistry.iterator();
INodeAdapterFactory factory = null;
while (iterator.hasNext()) {
factory = (INodeAdapterFactory) iterator.next();
if (factory.isFactoryForType(type)) {
result = factory.adapt(notifier);
break;
}
}
}
}
return result;
}
/**
* @see PageDirectiveAdapter#getEmbeddedType()
*/
public EmbeddedTypeHandler getEmbeddedType() {
if (embeddedTypeHandler == null) {
embeddedTypeHandler = getDefaultEmbeddedType();
}
return embeddedTypeHandler;
}
public void addEmbeddedFactory(INodeAdapterFactory factory) {
// should we check if already exists in list?
embeddedFactoryRegistry.add(factory);
}
// /**
// * Used by PageDirectiveWatchers to signal that some important attribute
// has changed, and
// * any cached values should be re-calcuated
// */
// void changed() {
// // we won't actually check if change is needed, if the model state is
// already changing.
// if (!model.isReinitializationNeeded()) {
// // go through our list of page watcher adapters, and updates the
// attributes
// // we're interested in, if and only if they are the earliest occurance
// in the resource
// String potentialContentType = null;
// String potentialLanguage = null;
// int contentTypePosition = -1;
// int languagePosition = -1;
// Iterator iterator = pageDirectiveWatchers.iterator();
// while (iterator.hasNext()) {
// PageDirectiveWatcher pdWatcher = (PageDirectiveWatcher)
// iterator.next();
// String contentType = pdWatcher.getContentType();
// String language = pdWatcher.getLanguage();
// int offset = pdWatcher.getOffset();
// if (potentialContentType == null || (hasValue(contentType) && (offset <
// contentTypePosition))) {
// potentialContentType = contentType;
// contentTypePosition = offset;
// }
// }
// // now we have the best candiates for cached values, let's see if
// they've really changed from
// // what we had. If so, note we go through the setters so side effects
// can take place there.
// potentialContentType =
// getMimeTypeFromContentTypeValue(potentialContentType);
// if (potentialContentType == null || potentialContentType.length() == 0)
// {
// //potentialContentType = getDefaultContentType();
// } else {
// setCachedContentType(potentialContentType);
// }
//
// if (potentialLanguage != null && hasValue(potentialLanguage)) {
// setCachedLanguage(potentialLanguage);
// }
// }
// }
void changedContentType(int elementOffset, String newValue) {
// only need to process if this new value is
// earlier in the file than our current value
if (firstContentTypePosition == -1 || elementOffset <= firstContentTypePosition) {
// dw_TODO: update embedded partitioner in JSP document
// partitioner
// nsd_TODO: update embedded partitioner in JSP document
// partitioner
// no need to change current value, if we're told some
// earlier value is null or blank (sounds like an error, anyway)
if (hasValue(newValue)) {
firstContentTypePosition = elementOffset;
String potentialContentType = getMimeTypeFromContentTypeValue(newValue);
// only do the set processing if different
// from what it already is
// if (!potentialContentType.equalsIgnoreCase(cachedLanguage))
// {
setCachedContentType(potentialContentType);
// }
}
}
}
/**
* Used by PageDirectiveWatchers to signal that some important attribute
* has changed, and any cached values should be re-calcuated
*/
void changedLanguage(int elementOffset, String newValue) {
// only need to process if this new value is
// earlier in the file than our current value
// has to be less than or equal to, in case our previous earliest one,
// is itself changing!
if (firstLanguagePosition == -1 || elementOffset <= firstLanguagePosition) {
// no need to change current value, if we're told some
// earlier value is null or blank (sounds like an error, anyway)
if (hasValue(newValue)) {
firstLanguagePosition = elementOffset;
// only do the set processing if different
// from what it already is
if (!newValue.equalsIgnoreCase(cachedLanguage)) {
setCachedLanguage(newValue);
}
}
// dw_TODO: set language in document partitioner
// nsd_TODO: set language in document partitioner
}
}
/**
* Used by PageDirectiveWatchers to signal that some important attribute
* has changed, and any cached values should be re-calcuated
*/
void changedPageEncoding(int elementOffset, String newValue) {
// we don't currently track active value, since
// just need during read and write (where its
// calculated. We will need in future, to
// acurately clone a model and to display
// "current encoding" to user in status bar.
}
/**
* Method hasValue.
*
* @param contentType
* @return boolean
*/
private boolean hasValue(String value) {
if (value != null && value.length() > 0)
return true;
else
return false;
}
/**
* Returns the cachedContentType.
*
* @return String
*/
public String getContentType() {
if (cachedContentType == null) {
cachedContentType = getDefaultContentType();
}
return cachedContentType;
}
/**
* Method getDefaultContentType.
*
* @return String
*/
private String getDefaultContentType() {
String type = null;
IFile file = getFile(model);
if (file != null) {
type = JSPFContentProperties.getProperty(JSPFContentProperties.JSPCONTENTTYPE, file, true);
}
// BUG136468
if (type == null)
type = "text/html"; //$NON-NLS-1$
return type;
}
/**
* Returns the cachedLanguage.
*
* @return String
*/
public String getLanguage() {
if (cachedLanguage == null)
cachedLanguage = getDefaultLanguage();
return cachedLanguage;
}
/**
* Method getDefaultLanguage.
*
* @return String
*/
private String getDefaultLanguage() {
String language = null;
IFile file = getFile(model);
if (file != null) {
language = JSPFContentProperties.getProperty(JSPFContentProperties.JSPLANGUAGE, file, true);
}
// BUG136468
if (language == null)
language = "java"; //$NON-NLS-1$
return language;
}
/**
* Sets the cachedContentType.
*
* @param cachedContentType
* The cachedContentType to set
*/
public void setCachedContentType(String newContentType) {
/*
* if the passed in value is the same as existing, there's nothing to
* do. if its different, then we need to change the contentHandler as
* well and, more to the point, signal a re-initializtation is needed.
*
* Note: if the value we're getting set to does not have a handler in
* the registry, we'll actually not set it to null or anything, we'll
* just continue on with the one we have. This is pretty important to
* avoid re-initializing on every key stroke if someone is typing in a
* new content type, but haven't yet finished the whole "word".
* However, if an contentType is not recognized, the registry returns
* the one for XML.
*/
/* set the actual value first, the rest is "side effect" */
this.cachedContentType = newContentType;
/* see if we need to update embedded handler */
/*
* If the document is a type of XHTML, we do not use the page
* directive's contentType to determine the embedded type ... its
* XHTML! ... and, eventually, the DOCTYPE adapter should determine
* if/when it needs to change.
*/
/* just safety check, can be removed later, early in release cycle */
if (model == null) {
// throw IllegalStateException("model should never be null in
// PageDirective Adapter");
Logger.log(Logger.ERROR, "model should never be null in PageDirective Adapter");
return;
}
EmbeddedTypeHandler potentialNewandler = null;
IContentDescription contentDescription = getContentDescription(model.getStructuredDocument());
Object prop = contentDescription.getProperty(IContentDescriptionForJSP.CONTENT_FAMILY_ATTRIBUTE);
if (prop != null) {
if (ContentTypeFamilyForHTML.HTML_FAMILY.equals(prop)) {
potentialNewandler = EmbeddedTypeRegistryImpl.getInstance().getTypeFor("text/html");
}
}
if (potentialNewandler == null) {
/*
* getHandler should always return something (never null), based
* on the rules in the factory.
*/
potentialNewandler = getHandlerFor(this.cachedContentType);
}
/*
* we do this check for re-init here, instead of in setEmbeddedType,
* since setEmbeddedType is called during the normal initializtion
* process, when re-init is not needed (since there is no content)
*/
if (embeddedTypeHandler == null) {
setEmbeddedType(potentialNewandler);
}
else if (potentialNewandler != null && embeddedTypeHandler != potentialNewandler) {
/*
* changing this embedded handler here may be in the middle of a
* notify loop. That's why we set that "it's needed". Then the
* model decides when its "safe" to actually do the re-init.
*
* be sure to hold oldHandler in temp var or else setEmbeddedType
* will "reset" it before modelReinitNeeded(oldHandler, handler)
* is called
*
*/
EmbeddedTypeHandler oldHandler = embeddedTypeHandler;
setEmbeddedType(potentialNewandler);
modelReinitNeeded(oldHandler, potentialNewandler);
}
}
/**
* This method is used to re-init based on embeddedTypeHandler changing.
* It is given priority over the language change, since there its more
* important to have old and new handlers's in the stateData field.
*/
private void modelReinitNeeded(EmbeddedTypeHandler oldHandler, EmbeddedTypeHandler newHandler) {
if (model.isReinitializationNeeded()) {
System.out.println("already being initialized"); //$NON-NLS-1$
}
try {
model.aboutToChangeModel();
model.setReinitializeStateData(new EmbeddedTypeStateData(oldHandler, newHandler));
model.setReinitializeNeeded(true);
}
finally {
model.changedModel();
}
}
/**
* Method modelReinitNeeded.
*/
private void modelReinitNeeded(String oldlanguage, String newLanguage) {
// bit of a short cut for now .... we dont' need language at the
// moment,
// but should set the state data
if (model.isReinitializationNeeded()) {
if (Debug.displayWarnings) {
System.out.println("already being initialized"); //$NON-NLS-1$
}
}
else {
try {
// if already being re-initialized, we don't want to
// reset the data in the stateData field.
model.aboutToChangeModel();
model.setReinitializeStateData(newLanguage);
model.setReinitializeNeeded(true);
}
finally {
model.changedModel();
}
}
}
public void setCachedLanguage(String newLanguage) {
if (cachedLanguage != null && languageStateChanged(cachedLanguage, newLanguage)) {
/*
* a complete re-init overkill in current system, since really
* just need for the line style providers, BUT, a change in
* language could effect other things, and we don't expect to
* happen often so a little overkill isn't too bad. The deep
* problem is that there is no way to get at the "edit side"
* adpapters specifically here in model class. we have to do the
* model changed sequence to get the screen to update. do not
* signal again, if signaled once (the reinit state data will be
* wrong. (this needs to be improved in future)
*/
if (!model.isReinitializationNeeded()) {
modelReinitNeeded(cachedLanguage, newLanguage);
}
}
setLanguage(newLanguage);
}
/**
* This is public access method, used especially from loader, for JSP
* Fragment support.
*/
public void setLanguage(String newLanguage) {
this.cachedLanguage = newLanguage;
IDocumentPartitioner partitioner = ((IDocumentExtension3) model.getStructuredDocument()).getDocumentPartitioner(IStructuredPartitioning.DEFAULT_STRUCTURED_PARTITIONING);
if (partitioner instanceof StructuredTextPartitionerForJSP) {
((StructuredTextPartitionerForJSP) partitioner).setLanguage(newLanguage);
}
}
/**
* Method languageStateChange.
*
* @param cachedLanguage
* @param newLanguage
* @return boolean
*/
private boolean languageStateChanged(String cachedLanguage, String newLanguage) {
boolean result = false; // languages are equal, then no change in
// state
if (!cachedLanguage.equalsIgnoreCase(newLanguage)) {
boolean oldLanguageKnown = languageKnown(cachedLanguage);
boolean newLanguageKnown = languageKnown(newLanguage);
result = newLanguageKnown || (!newLanguageKnown && oldLanguageKnown);
}
return result;
}
/**
* Method languageKnown.
*
* @param cachedLanguage
* @return boolean
*/
private boolean languageKnown(String language) {
return (StringUtils.contains(JAVA_LANGUAGE_KEYS, language, false) || StringUtils.contains(JAVASCRIPT_LANGUAGE_KEYS, language, false));
}
private IFile getFile(IStructuredModel model) {
String location = model.getBaseLocation();
if (location != null) {
IPath path = new Path(location);
if (path.segmentCount() > 1) {
return ResourcesPlugin.getWorkspace().getRoot().getFile(path);
}
}
return null;
}
private EmbeddedTypeHandler getHandlerFor(String contentType) {
EmbeddedTypeRegistry reg = getEmbeddedContentTypeRegistry();
EmbeddedTypeHandler handler = null;
if (reg != null)
handler = reg.getTypeFor(contentType);
return handler;
}
/**
* Gets the embeddedContentTypeRegistry.
*
* @return Returns a EmbeddedContentTypeRegistry
*/
private EmbeddedTypeRegistry getEmbeddedContentTypeRegistry() {
return EmbeddedTypeRegistryImpl.getInstance();
}
/**
* For JSP files, text/html is the default content type. This may want
* this different for types like jsv (jsp for voice xml) For now, hard
* code to new instance. In future, should get instance from registry.
*
* Specification cites HTML as the default contentType.
*/
protected EmbeddedTypeHandler getDefaultEmbeddedType() {
return getHandlerFor(getDefaultContentType());
}
public INodeNotifier getTarget() {
return notifierAtCreation;
}
public void release() {
if (embeddedTypeHandler != null) {
if (embeddedFactoryRegistry != null) {
Iterator iterator = embeddedFactoryRegistry.iterator();
INodeAdapterFactory factory = null;
while (iterator.hasNext()) {
factory = (INodeAdapterFactory) iterator.next();
factory.release();
}
}
// pa_TODO: possibly need to release here...
// or "uninitializeFactoryRegistry"
// initializeFactoryRegistry was called from JSPModelLoader
embeddedTypeHandler = null;
}
}
private IContentDescription getContentDescription(IDocument doc) {
if (doc == null)
return null;
DocumentReader in = new DocumentReader(doc);
return getContentDescription(in);
}
/**
* Returns content description for an input stream Assumes it's JSP
* content. Closes the input stream when finished.
*
* @param in
* @return the IContentDescription for in, or null if in is null
*/
private IContentDescription getContentDescription(Reader in) {
if (in == null)
return null;
IContentDescription desc = null;
try {
IContentType contentTypeJSP = Platform.getContentTypeManager().getContentType(ContentTypeIdForJSP.ContentTypeID_JSP);
desc = contentTypeJSP.getDescriptionFor(in, IContentDescription.ALL);
}
catch (IOException e) {
Logger.logException(e);
}
finally {
try {
in.close();
}
catch (IOException e) {
Logger.logException(e);
}
}
return desc;
}
public String getElIgnored() {
return elIgnored;
}
public void setElIgnored(String ignored) {
elIgnored = ignored;
}
}