blob: 377d968a33507213d4997ce5b227c67e4afd399d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.j2ee.common;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModel;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModelAction;
import org.openide.util.ChangeSupport;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
/**
* An utility class for working with metadata models in UI. Since model operations
* might take a long time, the model access should not take place in
* the event dispatching thread. Also, there should be status messages in the UI
* informing the user about the current state ("Classpath scanning in progress",
* "Reading entity classes", etc.). Writing code that does this
* is a difficult task and the code tends to be prone to errors. This class attempts to
* simplify this task.
*
* <p>This class can be used to execute an action while ensuring that it
* is executed outside the AWT thread and that all states (classpath scanning,
* reading the model) are handled. The state is encapsulated by the {@link #State}
* enumeration, which can be one of:</p>
*
* <ul>
* <li><code>IDLE</code> before the {@link #start} method has been called.</li>
* <li><code>WAITING_READY</code> waiting for the model to become {@link MetadataModel#isReady ready}.
* This e.g corresponds to the classpath scan being in progress for a metadata model
* based on Java annotations.</li>
* <li><code>READING_MODEL</code> reading the model.</li>
* <li><code>FINISHED</code> the action has finished executing. The result of the
* action can be {@link #getResult retrieved}.
* </ul>
*
* <p>For example, this is how the class would be used to display the entity
* classes in a wizard:</p>
*
* <pre>
* // the model to read from
* private MetadataModel<EntityMappingsMetadata> model;
* // the read helper
* private MetadataModelReadHelper<EntityMappingsMetadata, List<String>> readHelper;
* // the result of the model action (what will be read from the model)
* private List<String> entityNames;
*
* // ...
*
* public void readSettings(Object settings) {
* wizardDescriptor = (WizardDescriptor)settings;
* // ensure the read only takes place the first time readSettings() is called
* if (model == null) {
* // get the model (from a wizard property or in whatever way)
* model = (MetadataModel<EntityMappingsMetadata>)wizardDescriptor.getProperty("model"); // NOI18N
* // pass a MetadataModelAction to the read helper -- the same action
* // that would be passed directly to MetadataModel.runReadActionWhenReady()
* readHelper = MetadataModelReadHelper.create(model, new MetadataModelAction<EntityMappingsMetadata, List<String>>() {
* public List<String> run(EntityMappingsMetadata metadata) throws Exception {
* List<String> result = new ArrayList<String>();
* for (Entity entity : metadata.getRoot().getEntity()) {
* result.add(entity.getName());
* }
* return result;
* }
* });
* // need to listen on the read helper for state changes
* // and forward them as wizard state changes, which will cause
* // the isValid() method to be called
* readHelper.addChangeListener(new ChangeListener() {
* public void stateChanged(ChangeEvent e) {
* fireChange();
* }
* });
* // everything is set up, so start the read helper
* // the helper now starts executing the action in another thread
* readHelper.start();
* }
* }
*
* // ...
*
* public boolean isValid() {
* // test the read helper state
* switch (readHelper.getState()) {
* case WAITING_READY:
* setErrorMessage("Scanning classpath.");
* return false;
*
* case READING_MODEL:
* setErrorMessage("Searching entity classes.");
* return false;
*
* case FINISHED:
* // when we got here the model action has been executed
* if (entityNames == null) {
* try {
* entityNames = readHelper.getResult();
* // pass the entity names to the wizard's visual panel
* getComponent().setEntityNames(entityNames);
* } catch (ExecutionException e) {
* Exceptions.printStackTrace(e);
* }
* }
* }
* if (entityNames != null) {
* if (entityNames.size() == 0) {
* setErrorMessage("No entities");
* return false;
* }
* }
* setErrorMessage(" ");
* return true;
* }
* </pre>
*
* @since 1.17
* @author Andrei Badea
*/
public class MetadataModelReadHelper<T, R> {
public enum State { IDLE, WAITING_READY, READING_MODEL, FINISHED };
// needs to have a throughput of 1 to ensure events are queued
private final RequestProcessor eventRP = new RequestProcessor("MetadataModelReadHelper", 1); // NOI18N
private final RequestProcessor executorRP = new RequestProcessor("MetadataModelReadHelperExecutor", 3); // NOI18N
private final ChangeSupport changeSupport = new ChangeSupport(this);
private final MetadataModel<T> model;
private final MetadataModelAction<T, R> action;
private Reader reader;
private volatile State state = State.IDLE;
private volatile R result;
private volatile ExecutionException executionException;
/**
* Creates a new instance of <code>MetadataModelReadHelper</code>.
*
* @param model a metadata model; cannot be null.
* @param action an action to be executed by <code>model</code>.
*/
public static <T, R> MetadataModelReadHelper<T, R> create(MetadataModel<T> model, MetadataModelAction<T, R> action) {
Parameters.notNull("model", model); // NOI18N
Parameters.notNull("action", action); // NOI18N
return new MetadataModelReadHelper<T, R>(model, action);
}
private MetadataModelReadHelper(MetadataModel<T> model, MetadataModelAction<T, R> action) {
this.model = model;
this.action = action;
}
/**
* Adds a change listener. Whenever the state changes a change event is
* fired to the registered listeners.
*/
public void addChangeListener(ChangeListener listener) {
changeSupport.addChangeListener(listener);
}
/**
* Removes a change listener.
*/
public void removeChangeListener(ChangeListener listener) {
changeSupport.removeChangeListener(listener);
}
/**
* Starts the helper. This method may only be called once.
*/
public void start() {
synchronized (this) {
if (reader != null) {
throw new IllegalStateException("The action has already been started"); // NOI18N
}
reader = new Reader();
}
// ensure that the client sees the WAITING_READY state
// even if the eventRP task is queued
state = State.WAITING_READY;
changeSupport.fireChange();
// ensure the model is not accessed in the calling thread in case
// the calling thread is the AWT thread
executorRP.post(new Runnable() {
public void run() {
reader.run();
}
});
}
/**
* Returns the state of the helper; never null.
*/
public State getState() {
return state;
}
/**
* Returns the result of the action. This method may only be called
* when the state is {@link State#FINISHED}.
*/
public R getResult() throws ExecutionException {
if (state != State.FINISHED) {
throw new IllegalStateException("The action has not finished yet"); // NOI18N
}
if (executionException != null) {
throw executionException;
}
return result;
}
/**
* Fires change events asynchronously to avoid calling listeners under any
* locks that the model may take while executing actions (e.g., the JavaSource
* javac lock).
*/
private void fireChange() {
eventRP.post(new Runnable() {
public void run() {
changeSupport.fireChange();
}
});
}
private final class Reader {
public void run() {
try {
Future<Void> future = model.runReadActionWhenReady(new MetadataModelAction<T, Void>() {
public Void run(T metadata) throws Exception {
state = State.READING_MODEL;
fireChange();
result = action.run(metadata);
state = State.FINISHED;
fireChange();
return null;
}
});
// get any exceptions thrown if the action was run asynchronously
future.get();
} catch (Exception e) {
state = State.FINISHED;
executionException = new ExecutionException(e.getMessage(), e);
fireChange();
}
}
}
}