/******************************************************************************* * Copyright (c) 2016 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 * Jens Lukowski/Innoopract - initial renaming/restructuring * Angelo Zerr <angelo.zerr@gmail.com> - copied from org.eclipse.wst.xml.core.internal.document.DOMModelImpl * modified in order to process JSON Objects. * Alina Marin <alina@mx1.ibm.com> - fixed some stuff to improve the synch between the editor and the model. *******************************************************************************/ package org.eclipse.wst.json.core.internal.document; import org.eclipse.wst.json.core.document.IJSONArray; import org.eclipse.wst.json.core.document.IJSONDocument; import org.eclipse.wst.json.core.document.IJSONModel; import org.eclipse.wst.json.core.document.IJSONNode; import org.eclipse.wst.json.core.document.IJSONObject; import org.eclipse.wst.json.core.document.IJSONPair; import org.eclipse.wst.json.core.document.IJSONValue; import org.eclipse.wst.json.core.internal.Logger; import org.eclipse.wst.json.core.regions.JSONRegionContexts; import org.eclipse.wst.sse.core.internal.model.AbstractStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.events.IStructuredDocumentListener; import org.eclipse.wst.sse.core.internal.provisional.events.NewDocumentEvent; import org.eclipse.wst.sse.core.internal.provisional.events.NoChangeEvent; import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent; import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent; import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegionList; import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; import org.w3c.dom.Document; /** * SSE {@link IStructuredDocument} implementation for JSON. */ public class JSONModelImpl extends AbstractStructuredModel implements IStructuredDocumentListener, IJSONModel { private static String TRACE_PARSER_MANAGEMENT_EXCEPTION = "parserManagement"; //$NON-NLS-1$ private Object active = null; private JSONDocumentImpl document = null; private ISourceGenerator generator = null; private JSONModelNotifier notifier = null; private JSONModelParser parser = null; private boolean refresh = false; private JSONModelUpdater updater = null; /** * JSONModelImpl constructor */ public JSONModelImpl() { super(); this.document = (JSONDocumentImpl) internalCreateDocument(); } /** * This API allows clients to declare that they are about to make a "large" * change to the model. This change might be in terms of content or it might * be in terms of the model id or base location. * * Note that in the case of embedded calls, notification to listeners is * sent only once. * * Note that the client who is making these changes has the responsibility * to restore the models state once finished with the changes. See * getMemento and restoreState. * * The method isModelStateChanging can be used by a client to determine if * the model is already in a change sequence. */ public void aboutToChangeModel() { super.aboutToChangeModel(); // technically, no need to call beginChanging so often, // since aboutToChangeModel can be nested. // but will leave as is for this release. // see modelChanged, and be sure stays coordinated there. getModelNotifier().beginChanging(); } public void aboutToReinitializeModel() { JSONModelNotifier notifier = getModelNotifier(); notifier.cancelPending(); super.aboutToReinitializeModel(); } protected void pairReplaced(IJSONObject element, IJSONPair newAttr, IJSONPair oldAttr) { if (element == null) return; if (getActiveParser() == null) { JSONModelUpdater updater = getModelUpdater(); setActive(updater); updater.initialize(); updater.replaceAttr(element, newAttr, oldAttr); setActive(null); } getModelNotifier().pairReplaced(element, newAttr, oldAttr); } /** * This API allows a client controlled way of notifying all ModelEvent * listners that the model has been changed. This method is a matched pair * to aboutToChangeModel, and must be called after aboutToChangeModel ... or * some listeners could be left waiting indefinitely for the changed event. * So, its suggested that changedModel always be in a finally clause. * Likewise, a client should never call changedModel without calling * aboutToChangeModel first. * * In the case of embedded calls, the notification is just sent once. * */ public void changedModel() { // NOTE: the order of 'changedModel' and 'endChanging' is significant. // By calling changedModel first, this basically decrements the // "isChanging" counter // in super class and when zero all listeners to model state events // will be notified // that the model has been changed. 'endChanging' will notify all // deferred adapters. // So, the significance of order is that adapters (and methods they // call) // can count on the state of model "isChanging" to be accurate. // But, remember, that this means the "modelChanged" event can be // received before all // adapters have finished their processing. // NOTE NOTE: The above note is obsolete in fact (though still states // issue correctly). // Due to popular demand, the order of these calls were reversed and // behavior // changed on 07/22/2004. // // see also // https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=4302 // for motivation for this 'on verge of' call. // this could be improved in future if notifier also used counting // flag to avoid nested calls. If/when changed be sure to check if // aboutToChangeModel needs any changes too. if (isModelChangeStateOnVergeOfEnding()) { // end lock before noticiation loop, since directly or indirectly // we may be "called from foriegn code" during notification. endLock(); // we null out here to avoid spurious"warning" message while debug // tracing is enabled fLockObject = null; // the notifier is what controls adaper notification, which // should be sent out before the 'modelChanged' event. getModelNotifier().endChanging(); } // changedModel handles 'nesting', so only one event sent out // when mulitple calls to 'aboutToChange/Changed'. super.changedModel(); handleRefresh(); } /** * childReplaced method * * @param parentNode * org.w3c.dom.Node * @param newChild * org.w3c.dom.Node * @param oldChild * org.w3c.dom.Node */ protected void childReplaced(IJSONNode parentNode, IJSONNode newChild, IJSONNode oldChild) { if (parentNode == null) return; if (getActiveParser() == null) { JSONModelUpdater updater = getModelUpdater(); setActive(updater); updater.initialize(); updater.replaceChild(parentNode, newChild, oldChild); setActive(null); } getModelNotifier().childReplaced(parentNode, newChild, oldChild); } /** */ // protected void documentTypeChanged() { // if (this.refresh) // return; // // unlike 'resfresh', 'reinitialize' finishes loop // // and flushes remaining notification que before // // actually reinitializing. // // ISSUE: should reinit be used instead of handlerefresh? // // this.setReinitializeNeeded(true); // if (this.active != null || getModelNotifier().isChanging()) // return; // defer // handleRefresh(); // } // protected void editableChanged(Node node) { // if (node != null) { // getModelNotifier().editableChanged(node); // } // } /** */ // protected void endTagChanged(IJSONObject element) { // if (element == null) // return; // if (getActiveParser() == null) { // JSONModelUpdater updater = getModelUpdater(); // setActive(updater); // updater.initialize(); // // updater.changeEndTag(element); // setActive(null); // } // getModelNotifier().endTagChanged(element); // } /** */ private JSONModelParser getActiveParser() { if (this.parser == null) return null; if (this.parser != this.active) return null; return this.parser; } /** */ private JSONModelUpdater getActiveUpdater() { if (this.updater == null) return null; if (this.updater != this.active) return null; return this.updater; } @Override public Object getAdapter(Class adapter) { if (Document.class.equals(adapter)) return getDocument(); return super.getAdapter(adapter); } @Override public IJSONDocument getDocument() { return this.document; } public ISourceGenerator getGenerator() { if (this.generator == null) { this.generator = JSONGeneratorImpl.getInstance(); } return this.generator; } @Override public IndexedRegion getIndexedRegion(int offset) { if (this.document == null) return null; // search in document children IJSONNode parent = null; int length = this.document.getEndOffset(); if (offset * 2 < length) { // search from the first IJSONNode child = (IJSONNode) this.document.getFirstChild(); while (child != null) { if (child.getEndOffset() <= offset) { child = (IJSONNode) child.getNextSibling(); continue; } if (child.getStartOffset() > offset) { break; } IStructuredDocumentRegion startStructuredDocumentRegion = child .getStartStructuredDocumentRegion(); if (startStructuredDocumentRegion != null) { if (startStructuredDocumentRegion.getEnd() > offset) return child; } IStructuredDocumentRegion endStructuredDocumentRegion = child .getEndStructuredDocumentRegion(); if (endStructuredDocumentRegion != null) { if (endStructuredDocumentRegion.getStart() <= offset) { if (child instanceof IJSONPair) { IJSONValue value = ((IJSONPair)child).getValue(); if (value instanceof IJSONObject || value instanceof IJSONArray) { if (value.getStartOffset() < offset) { child = value; continue; } } } return child; } } // dig more parent = child; if (parent != null && parent.getNodeType() == IJSONNode.PAIR_NODE) { IJSONPair pair = (IJSONPair) parent; child = pair.getValue(); } else { child = (IJSONNode) parent.getFirstChild(); } } } else { // search from the last IJSONNode child = (IJSONNode) this.document.getLastChild(); while (child != null) { if (child.getStartOffset() > offset) { child = (IJSONNode) child.getPreviousSibling(); continue; } if (child.getEndOffset() <= offset) { break; } IStructuredDocumentRegion startStructuredDocumentRegion = child .getStartStructuredDocumentRegion(); if (startStructuredDocumentRegion != null) { if (startStructuredDocumentRegion.getEnd() > offset) return child; } IStructuredDocumentRegion endStructuredDocumentRegion = child .getEndStructuredDocumentRegion(); if (endStructuredDocumentRegion != null) { if (endStructuredDocumentRegion.getStart() <= offset) { if (child instanceof IJSONPair) { IJSONValue value = ((IJSONPair)child).getValue(); if (value instanceof IJSONObject || value instanceof IJSONArray) { if (value.getStartOffset() < offset) { child = value; continue; } } } return child; } } // dig more parent = child; if (parent != null && parent.getNodeType() == IJSONNode.PAIR_NODE) { IJSONPair pair = (IJSONPair) parent; child = pair.getValue(); } else { child = (IJSONNode) parent.getLastChild(); } } } return parent != null ? parent : document.getFirstChild(); } /** */ public JSONModelNotifier getModelNotifier() { if (this.notifier == null) { this.notifier = new JSONModelNotifierImpl(); } return this.notifier; } /** */ private JSONModelParser getModelParser() { if (this.parser == null) { this.parser = createModelParser(); } return this.parser; } protected JSONModelParser createModelParser() { return new JSONModelParser(this); } /** */ private JSONModelUpdater getModelUpdater() { if (this.updater == null) { this.updater = createModelUpdater(); } return this.updater; } protected JSONModelUpdater createModelUpdater() { return new JSONModelUpdater(this); } /** */ private void handleRefresh() { if (!this.refresh) return; JSONModelNotifier notifier = getModelNotifier(); boolean isChanging = notifier.isChanging(); if (!isChanging) notifier.beginChanging(true); JSONModelParser parser = getModelParser(); setActive(parser); this.document.removeChildNodes(); try { this.refresh = false; parser.replaceStructuredDocumentRegions(getStructuredDocument() .getRegionList(), null); } catch (Exception ex) { Logger.logException(ex); } finally { setActive(null); if (!isChanging) notifier.endChanging(); } } protected IJSONDocument internalCreateDocument() { JSONDocumentImpl document = new JSONDocumentImpl(); document.setModel(this); return document; } boolean isReparsing() { return (active != null); } @Override public void newModel(NewDocumentEvent structuredDocumentEvent) { if (structuredDocumentEvent == null) return; IStructuredDocument structuredDocument = structuredDocumentEvent .getStructuredDocument(); if (structuredDocument == null) return; // this should not happen, but for the case if (fStructuredDocument != null && fStructuredDocument != structuredDocument) setStructuredDocument(structuredDocument); internalSetNewDocument(structuredDocument); } private void internalSetNewDocument(IStructuredDocument structuredDocument) { if (structuredDocument == null) return; IStructuredDocumentRegionList flatNodes = structuredDocument .getRegionList(); if ((flatNodes == null) || (flatNodes.getLength() == 0)) { return; } if (this.document == null) return; // being constructed JSONModelUpdater updater = getActiveUpdater(); if (updater != null) { // being updated try { updater.replaceStructuredDocumentRegions(flatNodes, null); } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); } // // for new model, we might need to // // re-init, e.g. if someone calls setText // // on an existing model // checkForReinit(); return; } JSONModelNotifier notifier = getModelNotifier(); boolean isChanging = notifier.isChanging(); // call even if changing to notify doing new model getModelNotifier().beginChanging(true); JSONModelParser parser = getModelParser(); setActive(parser); this.document.removeChildNodes(); try { parser.replaceStructuredDocumentRegions(flatNodes, null); } catch (Exception ex) { Logger.logException(ex); // meaningless to refresh, because the result might be the same } finally { setActive(null); if (!isChanging) { getModelNotifier().endChanging(); } // ignore refresh this.refresh = false; } } /* * (non-Javadoc) * * @see org.eclipse.wst.sse.core.internal.provisional.events. * IStructuredDocumentListener * #noChange(org.eclipse.wst.sse.core.internal.provisional * .events.NoChangeEvent) */ @Override public void noChange(NoChangeEvent event) { JSONModelUpdater updater = getActiveUpdater(); if (updater != null) { // being updated // cleanup updater staffs try { updater.replaceStructuredDocumentRegions(null, null); } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); } // I guess no chanage means the model could not need re-init // checkForReinit(); return; } } /* * (non-Javadoc) * * @see org.eclipse.wst.sse.core.internal.provisional.events. * IStructuredDocumentListener * #nodesReplaced(org.eclipse.wst.sse.core.internal * .provisional.events.StructuredDocumentRegionsReplacedEvent) */ @Override public void nodesReplaced(StructuredDocumentRegionsReplacedEvent event) { if (event == null) return; IStructuredDocumentRegionList oldStructuredDocumentRegions = event .getOldStructuredDocumentRegions(); IStructuredDocumentRegionList newStructuredDocumentRegions = event .getNewStructuredDocumentRegions(); JSONModelUpdater updater = getActiveUpdater(); if (updater != null) { // being updated try { updater.replaceStructuredDocumentRegions( newStructuredDocumentRegions, oldStructuredDocumentRegions); } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); } // checkForReinit(); return; } JSONModelNotifier notifier = getModelNotifier(); boolean isChanging = notifier.isChanging(); if (!isChanging) notifier.beginChanging(); JSONModelParser parser = getModelParser(); setActive(parser); try { /* workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=486860 */ // this.refresh = true; // handleRefresh(); boolean reloadModel = false; // Check if the insertion is between two previously existing JSON Nodes, in that case // the model is reloaded completely. if (newStructuredDocumentRegions != null) { int newCount = newStructuredDocumentRegions.getLength(); for (int i = 0; i < newCount -1; i++) { if (newStructuredDocumentRegions.item(i).getType().equals(JSONRegionContexts.JSON_COMMA)) { IStructuredDocumentRegion nextNode = newStructuredDocumentRegions.item(i).getNext(); while (nextNode != null) { if (nextNode.getType().equals(JSONRegionContexts.JSON_OBJECT_KEY) || nextNode.getType().equals(JSONRegionContexts.JSON_OBJECT_OPEN) || nextNode.getType().equals(JSONRegionContexts.JSON_ARRAY_OPEN)) { reloadModel = true; break; } nextNode = nextNode.getNext(); } } } } if (!reloadModel && oldStructuredDocumentRegions != null && oldStructuredDocumentRegions.getLength() > 0) { if (oldStructuredDocumentRegions.getLength() > 3 || oldStructuredDocumentRegions.item(0).getType().equals(JSONRegionContexts.JSON_OBJECT_OPEN)) { // Reload all the model when the first region that will be // replaced is a JSONObject or if more than 3 regions are // replaced in the model (a JSONPair is composed by 3 regions, // this means more that one JSON Pair are replaced in the model) reloadModel = true; } else { // also, always reload the model in case of removing at least one UNDEFINED region for (int i = 0; !reloadModel && i < oldStructuredDocumentRegions.getLength(); i++) { if (oldStructuredDocumentRegions.item(i).getType().equals(JSONRegionContexts.UNDEFINED)) { reloadModel = true; } } } } if(reloadModel) { this.refresh = true; handleRefresh(); } else { parser.replaceStructuredDocumentRegions(newStructuredDocumentRegions, oldStructuredDocumentRegions); } } catch (Exception ex) { if (ex.getClass().equals( StructuredDocumentRegionManagementException.class)) { Logger.traceException(TRACE_PARSER_MANAGEMENT_EXCEPTION, ex); } else { Logger.logException(ex); } this.refresh = true; handleRefresh(); } finally { setActive(null); if (!isChanging) { notifier.endChanging(); handleRefresh(); } } } /* * (non-Javadoc) * * @see org.eclipse.wst.sse.core.internal.provisional.events. * IStructuredDocumentListener * #regionChanged(org.eclipse.wst.sse.core.internal * .provisional.events.RegionChangedEvent) */ @Override public void regionChanged(RegionChangedEvent event) { if (event == null) return; IStructuredDocumentRegion flatNode = event .getStructuredDocumentRegion(); if (flatNode == null) return; ITextRegion region = event.getRegion(); if (region == null) return; JSONModelUpdater updater = getActiveUpdater(); if (updater != null) { // being updated try { updater.changeRegion(event, flatNode, region); } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); } // checkForReinit(); return; } JSONModelNotifier notifier = getModelNotifier(); boolean isChanging = notifier.isChanging(); if (!isChanging) notifier.beginChanging(); JSONModelParser parser = getModelParser(); setActive(parser); try { parser.changeRegion(event, flatNode, region); /* workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=486860 */ // this.refresh = true; // handleRefresh(); } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); if (!isChanging) { notifier.endChanging(); handleRefresh(); } } // checkForReinit(); } /* * (non-Javadoc) * * @see org.eclipse.wst.sse.core.internal.provisional.events. * IStructuredDocumentListener * #regionsReplaced(org.eclipse.wst.sse.core.internal * .provisional.events.RegionsReplacedEvent) */ @Override public void regionsReplaced(RegionsReplacedEvent event) { if (event == null) return; IStructuredDocumentRegion flatNode = event .getStructuredDocumentRegion(); if (flatNode == null) return; ITextRegionList oldRegions = event.getOldRegions(); ITextRegionList newRegions = event.getNewRegions(); if (oldRegions == null && newRegions == null) return; JSONModelUpdater updater = getActiveUpdater(); if (updater != null) { // being updated try { updater.replaceRegions(flatNode, newRegions, oldRegions); } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); } // checkForReinit(); return; } JSONModelNotifier notifier = getModelNotifier(); boolean isChanging = notifier.isChanging(); if (!isChanging) notifier.beginChanging(); JSONModelParser parser = getModelParser(); setActive(parser); try { /* workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=486860 */ // this.refresh = true; // handleRefresh(); boolean reloadModel = false; // Check if the insertion is between two previously existing JSON Nodes, in that case // the model is reloaded completely. if (flatNode.getType().equals(JSONRegionContexts.JSON_COMMA)) { IStructuredDocumentRegion nextNode = flatNode.getNext(); while (nextNode != null) { if (nextNode.getType().equals(JSONRegionContexts.JSON_OBJECT_KEY) || nextNode.getType().equals(JSONRegionContexts.JSON_OBJECT_OPEN) || nextNode.getType().equals(JSONRegionContexts.JSON_ARRAY_OPEN)) reloadModel = true; nextNode = nextNode.getNext(); } } if(reloadModel) { this.refresh = true; handleRefresh(); } else { parser.replaceRegions(flatNode, newRegions, oldRegions); } } catch (Exception ex) { Logger.logException(ex); this.refresh = true; handleRefresh(); } finally { setActive(null); if (!isChanging) { notifier.endChanging(); handleRefresh(); } } // checkForReinit(); } /** */ public void releaseFromEdit() { if (!isShared()) { // this.document.releaseStyleSheets(); // this.document.releaseDocumentType(); } super.releaseFromEdit(); } /** */ public void releaseFromRead() { if (!isShared()) { // this.document.releaseStyleSheets(); // this.document.releaseDocumentType(); } super.releaseFromRead(); } /** */ private void setActive(Object active) { this.active = active; // side effect // when ever becomes active, besure tagNameCache is cleared // (and not used) // if (active == null) { // document.activateTagNameCache(true); // } else { // document.activateTagNameCache(false); // } } /** */ // public void setGenerator(ISourceGenerator generator) { // this.generator = generator; // } /** */ public void setModelNotifier(JSONModelNotifier notifier) { this.notifier = notifier; } /** */ public void setModelParser(JSONModelParser parser) { this.parser = parser; } /** */ public void setModelUpdater(JSONModelUpdater updater) { this.updater = updater; } /** * setStructuredDocument method * * @param structuredDocument */ public void setStructuredDocument(IStructuredDocument structuredDocument) { IStructuredDocument oldStructuredDocument = super .getStructuredDocument(); if (structuredDocument == oldStructuredDocument) return; // nothing to do if (oldStructuredDocument != null) oldStructuredDocument.removeDocumentChangingListener(this); super.setStructuredDocument(structuredDocument); if (structuredDocument != null) { internalSetNewDocument(structuredDocument); structuredDocument.addDocumentChangingListener(this); } } /** */ protected void startTagChanged(IJSONObject element) { if (element == null) return; if (getActiveParser() == null) { JSONModelUpdater updater = getModelUpdater(); setActive(updater); updater.initialize(); updater.changeStartTag(element); setActive(null); } getModelNotifier().startTagChanged(element); } protected void valueChanged(IJSONNode node) { if (node == null) return; if (getActiveParser() == null) { JSONModelUpdater updater = getModelUpdater(); setActive(updater); updater.initialize(); updater.changeValue(node); setActive(null); } getModelNotifier().valueChanged(node); } }