blob: 5a36d4310974d7761daba3742b2712887f709cbd [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.persistence.wizard;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.SourceUtils;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModel;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModelAction;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModelException;
import org.netbeans.modules.j2ee.metadata.model.api.support.annotation.PersistentObject;
import org.netbeans.modules.j2ee.persistence.api.EntityClassScope;
import org.netbeans.modules.j2ee.persistence.api.metadata.orm.Entity;
import org.netbeans.modules.j2ee.persistence.api.metadata.orm.EntityMappingsMetadata;
import org.netbeans.modules.j2ee.persistence.util.MetadataModelReadHelper;
import org.netbeans.modules.j2ee.persistence.util.MetadataModelReadHelper.State;
import org.netbeans.modules.j2ee.persistence.wizard.jpacontroller.JpaControllerUtil;
import org.openide.filesystems.FileObject;
import org.openide.util.ChangeSupport;
import org.openide.util.Exceptions;
/**
*
* @author Pavel Buzek
*/
public class EntityClosure {
private static final Logger LOG = Logger.getLogger(EntityClosure.class.getName());
// XXX this class needs a complete rewrite: the computing of the available
// entity classes and of the referenced classes need to be moved away.
private final ChangeSupport changeSupport = new ChangeSupport(this);
private Set<Entity> availableEntityInstances = new HashSet<Entity>();
private Set<String> availableEntities = new HashSet<String>();
private Set<String> wantedEntities = new HashSet<String>();
private Set<String> selectedEntities = new HashSet<String>();
private Set<String> referencedEntities = new HashSet<String>();
private HashMap<String,Entity> fqnEntityMap = new HashMap<String,Entity>();
private HashMap<String,Boolean> fqnIdExistMap = new HashMap<String,Boolean>();
private boolean modelReady;
private boolean ejbModuleInvolved = false;
private boolean closureEnabled = true;
private Project project;
private final MetadataModelReadHelper<EntityMappingsMetadata, List<Entity>> readHelper;
private final MetadataModel<EntityMappingsMetadata> model;
public static EntityClosure create(EntityClassScope entityClassScope, Project project) {
EntityClosure ec = new EntityClosure(entityClassScope, project);
ec.initialize();
return ec;
}
public static ComboBoxModel getAsComboModel(EntityClosure ec) {
return new EntityClosureComboBoxModel(ec);
}
private EntityClosure(EntityClassScope entityClassScope, Project project) {
this.model = entityClassScope.getEntityMappingsModel(true);
this.project = project;
readHelper = MetadataModelReadHelper.create(model, new MetadataModelAction<EntityMappingsMetadata, List<Entity>>() {
@Override
public List<Entity> run(EntityMappingsMetadata metadata) {
return Arrays.<Entity>asList(metadata.getRoot().getEntity());
}
});
}
private void initialize() {
readHelper.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (readHelper.getState() == State.FINISHED) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
addAvaliableEntities(new HashSet<Entity>(readHelper.getResult()));
modelReady = true;
changeSupport.fireChange();
} catch (ExecutionException e) {
Exceptions.printStackTrace(e);
}
}
});
}
}
});
readHelper.start();
}
public void addAvaliableEntities(final Set<Entity> entities) {
availableEntityInstances.addAll(entities);
JavaSource source = null;
try {
source = model.runReadAction(new MetadataModelAction<EntityMappingsMetadata, JavaSource>() {
@Override
public JavaSource run(EntityMappingsMetadata metadata) throws Exception {
return metadata.createJavaSource();
}
});
} catch (MetadataModelException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if( source!=null ) {
try {
ClassPath classPath = source.getClasspathInfo().getClassPath(ClasspathInfo.PathKind.COMPILE);
final Set<Project> ejbProjects = getEjbModulesOfClasspath(classPath);
source.runUserActionTask(new Task<CompilationController>() {
@Override
public void run(CompilationController parameter) throws Exception {
for (Entity en : entities) {
availableEntities.add(en.getClass2());
fqnEntityMap.put(en.getClass2(), en);
PersistentObject po = (PersistentObject) en;
ElementHandle<TypeElement> teh = po.getTypeElementHandle();
TypeElement te = teh.resolve(parameter);
fqnIdExistMap.put(en.getClass2(), JpaControllerUtil.haveId(te));
// issue #219565 - troubles of EJB's enities CP visibility
FileObject file = SourceUtils.getFile(teh, parameter.getClasspathInfo());
if (isEjbProjectEntity(ejbProjects, file)) {
ejbModuleInvolved = true;
LOG.log(Level.INFO, "Entity came from EJB module and mustn''t be visible on CP, entity={0}", en.getClass2());
}
}
}
}, true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
availableEntities.removeAll(selectedEntities);
changeSupport.fireChange();
}
private boolean isEjbProjectEntity(Set<Project> ejbProjects, FileObject fo) {
for (Project relatedProject : ejbProjects) {
if (fo.getPath().startsWith(relatedProject.getProjectDirectory().getPath())) {
return true;
}
}
return false;
}
public void addChangeListener(ChangeListener listener) {
changeSupport.addChangeListener(listener);
}
public Set<String> getAvailableEntities() {
return availableEntities;
}
public Set<Entity> getAvailableEntityInstances() {
return availableEntityInstances;
}
public Set<String> getWantedEntities() {
return wantedEntities;
}
public Set<String> getSelectedEntities() {
return selectedEntities;
}
public boolean isEjbModuleInvolved() {
return ejbModuleInvolved;
}
public void addEntities(Set<String> entities) {
if (isClosureEnabled()) {
if (wantedEntities.addAll(entities)) {
try{
Set<String> refEntities = getReferencedEntitiesTransitively(entities);
Set<String> addedEntities = new HashSet<String>(entities);
addedEntities.addAll(refEntities);
selectedEntities.addAll(addedEntities);
referencedEntities.addAll(refEntities);
availableEntities.removeAll(addedEntities);
changeSupport.fireChange();
} catch (IOException ioe){
Exceptions.printStackTrace(ioe);
}
}
} else {
wantedEntities.addAll(entities);
selectedEntities.addAll(entities);
availableEntities.removeAll(entities);
changeSupport.fireChange();
}
}
public void removeEntities(Set<String> entities) {
if (isClosureEnabled()) {
if (wantedEntities.removeAll(entities)) {
redoClosure();
changeSupport.fireChange();
}
} else {
wantedEntities.removeAll(entities);
selectedEntities.removeAll(entities);
availableEntities.addAll(entities);
changeSupport.fireChange();
}
}
public void addAllEntities() {
wantedEntities.addAll(availableEntities);
if (isClosureEnabled()) {
redoClosure();
changeSupport.fireChange();
} else {
selectedEntities.addAll(wantedEntities);
availableEntities.clear();
changeSupport.fireChange();
}
}
public void removeAllEntities() {
availableEntities.addAll(selectedEntities);
wantedEntities.clear();
selectedEntities.clear();
referencedEntities.clear();
changeSupport.fireChange();
}
/**
* Returns the tables transitively referenced by the contents of the tables parameter
* (not including tables passed in this parameter). If a table references itself,
* it is not added to the result.
*/
private Set<String> getReferencedEntitiesTransitively(Set<String> entities) throws IOException {
Queue<String> entityQueue = new Queue<String>(entities);
Set<String> refEntities = new HashSet<String>();
while (!entityQueue.isEmpty()) {
String entity = entityQueue.poll();
Set<String> referenced = getReferencedEntities(entity);
for (String refEntity : referenced) {
if (!refEntity.equals(entity)) {
refEntities.add(refEntity);
}
entityQueue.offer(refEntity);
}
}
return refEntities;
}
private Set<String> getReferencedEntities(final String entityClass) throws IOException {
if (readHelper.getState() != State.FINISHED) {
return Collections.emptySet();
}
JavaSource source = model.runReadAction(new MetadataModelAction<EntityMappingsMetadata, JavaSource>() {
@Override
public JavaSource run(EntityMappingsMetadata metadata) throws Exception {
return metadata.createJavaSource();
}
});
final Set<String> result = new HashSet<String>();
source.runUserActionTask(new Task<CompilationController>() {
@Override
public void run(CompilationController parameter) throws Exception {
List<Entity> entities = readHelper.getResult();
Set<String> entitiesFqn = new HashSet<String>();
for( Entity entity : entities){
entitiesFqn.add( entity.getClass2());
}
TypeElement entity = parameter.getElements().getTypeElement(entityClass);
for (Element element : parameter.getElements().getAllMembers(entity)){
if (ElementKind.METHOD.equals(element.getKind())){
ExecutableType methodType = (ExecutableType)parameter.getTypes().
asMemberOf((DeclaredType)entity.asType(), element);
TypeMirror returnType = methodType.getReturnType();
addTypeMirror(result, parameter, returnType, entitiesFqn);
List<? extends TypeMirror> parameterTypes =
methodType.getParameterTypes();
for (TypeMirror paramType : parameterTypes) {
addTypeMirror(result, parameter, paramType, entitiesFqn);
}
}
else if (ElementKind.FIELD.equals(element.getKind())){
TypeMirror typeMirror = parameter.getTypes().
asMemberOf((DeclaredType)entity.asType(), element);
addTypeMirror(result, parameter, typeMirror, entitiesFqn );
}
}
}
private void addTypeMirror( final Set<String> result,
CompilationController parameter, TypeMirror typeMirror ,
Set<String> allEntitiesFqn ) throws Exception
{
Element typeElement = parameter.getTypes().asElement(
typeMirror );
if ( typeElement instanceof TypeElement ){
String fqn = ((TypeElement)typeElement).getQualifiedName().toString();
if ( allEntitiesFqn.contains( fqn )){
result.add( fqn );
}
}
}
}, true);
return result;
}
private void redoClosure() {
Set<String> allEntities = new HashSet<String>(availableEntities);
allEntities.addAll(selectedEntities);
referencedEntities.clear();
try{
referencedEntities.addAll(getReferencedEntitiesTransitively(wantedEntities));
}catch (IOException ioe){
Exceptions.printStackTrace(ioe);
}
selectedEntities.clear();
selectedEntities.addAll(wantedEntities);
selectedEntities.addAll(referencedEntities);
availableEntities.clear();
availableEntities.addAll(allEntities);
availableEntities.removeAll(selectedEntities);
}
public boolean isClosureEnabled() {
return closureEnabled;
}
public void setClosureEnabled(boolean closureEnabled) {
if (this.closureEnabled == closureEnabled) {
return;
}
this.closureEnabled = closureEnabled;
if (closureEnabled) {
redoClosure();
} else {
Set<String> allEntities = new HashSet<String>(availableEntities);
allEntities.addAll(selectedEntities);
referencedEntities.clear();
selectedEntities.clear();
selectedEntities.addAll(wantedEntities);
availableEntities.clear();
availableEntities.addAll(allEntities);
availableEntities.removeAll(selectedEntities);
}
changeSupport.fireChange();
}
public boolean isModelReady() {
return modelReady;
}
void waitModelIsReady(){
try {
Future result = model.runReadActionWhenReady(new MetadataModelAction<EntityMappingsMetadata, Boolean>() {
@Override
public Boolean run(EntityMappingsMetadata metadata) throws Exception {
return true;
}
});
result.get();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (ExecutionException ex) {
Exceptions.printStackTrace(ex);
} catch (MetadataModelException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
private boolean isFieldAccess(final String entity) throws MetadataModelException, IOException {
Boolean result = model.runReadAction(new MetadataModelAction<EntityMappingsMetadata, Boolean>() {
@Override
public Boolean run(EntityMappingsMetadata metadata) throws Exception {
for (Entity e : metadata.getRoot().getEntity()){
if (e.getClass2().equals(entity)){
return e.getAccess().equals(Entity.FIELD_ACCESS);
}
}
return false;
}
});
return result;
}
Boolean haveId(String entityFqn) {
return fqnIdExistMap.get(entityFqn);
}
Entity getEntity(String entityFqn){
return fqnEntityMap.get(entityFqn);
}
private Set<Project> getEjbModulesOfClasspath(ClassPath classPath) {
Set<Project> ejbProjects = new HashSet<Project>();
for (FileObject fileObject : classPath.getRoots()) {
Project rootOwner = FileOwnerQuery.getOwner(fileObject);
if (rootOwner != null) {
if (Util.isEjbModule(rootOwner)) {
ejbProjects.add(rootOwner);
}
}
}
return ejbProjects;
}
/**
* A simple queue. An object can only be added once, even
* if it has already been removed from the queue. This class could implement
* the {@link java.util.Queue} interface, but it doesn't because that
* interface has too many unneeded methods. Not private because of the tests.
*/
static final class Queue<T> {
/**
* The queue. Implemented as ArrayList since will be iterated using get().
*/
private final List<T> queue;
/**
* The contents of the queue, needed in order to quickly (ideally
* in a constant time) tell if a table has been already added.
*/
private final Set<T> contents;
/**
* The position in the queue.
*/
private int currentIndex;
/**
* Creates a queue with an initial contents.
*/
public Queue(Set<T> initialContents) {
assert !initialContents.contains(null);
queue = new ArrayList(initialContents);
contents = new HashSet(initialContents);
}
/**
* Adds an elements to the queue if it hasn't been already added.
*/
public void offer(T element) {
assert element != null;
if (!contents.contains(element)) {
contents.add(element);
queue.add(element);
}
}
/**
* Returns the element at the top of the queue without removing it or null
* if the queue is empty.
*/
public boolean isEmpty() {
return currentIndex >= queue.size();
}
/**
* Returns and removes the elements at the top of the queue or null if
* the queue is empty.
*/
public T poll() {
T result = null;
if (!isEmpty()) {
result = queue.get(currentIndex);
currentIndex++;
}
return result;
}
}
private static class EntityClosureComboBoxModel extends DefaultComboBoxModel implements ChangeListener {
private EntityClosure entityClosure;
private List<String> entities = new ArrayList<String>();
EntityClosureComboBoxModel(EntityClosure entityClosure) {
this.entityClosure = entityClosure;
entityClosure.addChangeListener(this);
refresh();
}
@Override
public int getSize() {
return entities.size();
}
@Override
public Object getElementAt(int index) {
return entities.get(index);
}
/**
* @return the fully qualified names of the entities in this model.
*/
public List<String> getEntityClasses() {
return entities;
}
@Override
public void stateChanged(ChangeEvent e) {
refresh();
}
private void refresh() {
int oldSize = getSize();
entities = new ArrayList<String>(entityClosure.getAvailableEntities());
Collections.sort(entities);
fireContentsChanged(this, 0, Math.max(oldSize, getSize()));
}
}
}