/* * Copyright 2003-2017 JetBrains s.r.o. * * 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. */ package jetbrains.mps.text.impl; import jetbrains.mps.messages.IMessage; import jetbrains.mps.text.BufferSnapshot; import jetbrains.mps.text.CompatibilityTextUnit; import jetbrains.mps.text.TextBuffer; import jetbrains.mps.text.TextUnit; import jetbrains.mps.textgen.trace.ScopePositionInfo; import jetbrains.mps.textgen.trace.TraceablePositionInfo; import jetbrains.mps.textgen.trace.UnitPositionInfo; import jetbrains.mps.util.FileUtil; import jetbrains.mps.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SNode; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * General {@link TextUnit} implementation for use both Java and non-Java source code, as well as binary (Base64-encoded) units. * * @author Artem Tikhomirov */ public class RegularTextUnit implements TextUnit, CompatibilityTextUnit { private final SNode myStartNode; private final String myFilename; private final Charset myEncoding; private Status myState = Status.Undefined; private String myOutcome; private BufferLayoutConfiguration myLayoutBuilder; // CompatibilityTextUnit stuff private TraceInfoCollector myTraceCollector; private ErrorCollector myErrorCollector; private List<Pair<String,Object>> myContextObjects; public RegularTextUnit(@NotNull SNode root, @NotNull String filename) { this(root, filename, null); } public RegularTextUnit(@NotNull SNode root, @NotNull String filename, @Nullable Charset encoding) { myStartNode = root; myFilename = filename; myEncoding = encoding; myLayoutBuilder = new BufferLayoutConfiguration(); } public void setBufferLayout(@NotNull BufferLayoutConfiguration cfg) { checkNotYetGenerated(); myLayoutBuilder = cfg; } public void addContextObject(@NotNull String identity, @NotNull Object contextObject) { checkNotYetGenerated(); if (myContextObjects == null) { // it's a single thread we configure TU from myContextObjects = new ArrayList<>(2); } myContextObjects.add(new Pair<>(identity, contextObject)); } /** * XXX Perhaps, getAssociatedData(Class) would be better name? * * Access context object compatible with the supplied class. * Return the instance supplied through {@link #addContextObject(String, Object)}, if any. * * At the moment, compatible means {@link Class#isInstance(Object)}, we might introduce a mechanism * similar to IAdaptable in the future (i.e. each context object that is IAdaptable would get consulted IAdaptable.adapt() * in addition to isInstance()). */ @Nullable public <T> T findContextObject(Class<T> kind) { if (myContextObjects == null) { return null; } for (Pair<String, Object> p : myContextObjects) { if (kind.isInstance(p.o2)) { return kind.cast(p.o2); } } return null; } /** * @param identity key an object has been registered with in {@link #addContextObject(String, Object)} * @param kind object instance has to be instance of supplied class for the call to succeed * @param <T> context object type * @return registered context object or {@code null} if no record matching both identity key and Java class found. */ public <T> T getContextObject(@NotNull String identity, @NotNull Class<T> kind) { if (myContextObjects == null) { return null; } for (Pair<String, Object> p : myContextObjects) { if (identity.equals(p.o1) && kind.isInstance(p.o2)) { // XXX perhaps, can gracefully tell LD about incompatible types? return kind.cast(p.o2); } } return null; } @NotNull @Override public SNode getStartNode() { return myStartNode; } @NotNull @Override public String getFileName() { return myFilename; } @Override public void generate() { if (!TextGenRegistry.getInstance().hasTextGen(myStartNode)) { myState = Status.Empty; return; } myTraceCollector = new TraceInfoCollector(); addContextObject(TraceInfoCollector.class.getName(), myTraceCollector); TextBuffer trueBuffer = new TextBufferImpl(); myLayoutBuilder.prepareBuffer(trueBuffer); // if we got that far (tried to generate(), at least), do not consider state == undefined. // It's easy way to deal with uncaught exceptions from text generation and not to fail with assert state != Undefined in TextGen_Facet. // Proper way is likely to try/catch/re-throw here. myState = Status.Failed; myErrorCollector = new ErrorCollector(); TextGenTransitionContext tgContext = new TextGenTransitionContext(myStartNode, this, myErrorCollector, trueBuffer); TextGenSupport tgs = new TextGenSupport(tgContext); tgs.appendNode(myStartNode); final BufferSnapshot textSnapshot = myLayoutBuilder.prepareSnapshot(trueBuffer); myTraceCollector.populatePositions(textSnapshot); myOutcome = textSnapshot.getText().toString(); if (myErrorCollector.hasErrors()) { myState = Status.Failed; } else { myState = Status.Generated; } } @Override public byte[] getBytes() { if (myState == Status.Undefined) { throw new IllegalStateException("Shall generate first"); } // FIXME Handling of binary/base64 encoded strings missing?! // if (myEncoding == null && "binary".equals(getLegacyEncoding())) { // return EncodingUtil.decodeBase64(myOutcome); // } return myOutcome.getBytes(getEncoding()); } @Override public Charset getEncoding() { return myEncoding == null ? FileUtil.DEFAULT_CHARSET : myEncoding; } @Override public Status getState() { return myState; } /** * Until I decide whether to pass IMessageList into TU for it to report errors, or to supply * collection of messages to outer world, I don't want to expose this method in TextUnit API, and thus keep it here. * @return all the messages (anything that TU has reported to date) in order from older to newer. */ @NotNull public List<IMessage> getMessages() { return myErrorCollector == null ? Collections.emptyList() : myErrorCollector.problems(); } @Nullable public Map<SNode, TraceablePositionInfo> getPositions() { return myTraceCollector == null ? null : myTraceCollector.getTracePositions(); } @Nullable public Map<SNode, ScopePositionInfo> getScopePositions() { return myTraceCollector == null ? null : myTraceCollector.getScopePositions(); } @Nullable public Map<SNode, UnitPositionInfo> getUnitPositions() { return myTraceCollector == null ? null : myTraceCollector.getUnitPositions(); } private void checkNotYetGenerated() { assert myState == Status.Undefined : "Shall configure TU prior to generation"; } }