/******************************************************************************* * Copyright (c) 2016 BREDEX GmbH. * 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: * BREDEX GmbH - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.jubula.client.core.businessprocess; import java.util.HashSet; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.ArrayList; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import org.eclipse.jubula.client.core.businessprocess.ComponentNamesBP.CompNameCreationContext; import org.eclipse.jubula.client.core.businessprocess.db.TestSuiteBP; import org.eclipse.jubula.client.core.businessprocess.problems.ProblemType; import org.eclipse.jubula.client.core.businessprocess.treeoperations.CountCompNameUsage; import org.eclipse.jubula.client.core.events.DataChangedEvent; import org.eclipse.jubula.client.core.events.DataEventDispatcher; import org.eclipse.jubula.client.core.events.DataEventDispatcher.DataState; import org.eclipse.jubula.client.core.events.DataEventDispatcher.UpdateState; import org.eclipse.jubula.client.core.model.IAUTMainPO; import org.eclipse.jubula.client.core.model.IComponentNamePO; import org.eclipse.jubula.client.core.model.IObjectMappingAssoziationPO; import org.eclipse.jubula.client.core.model.IProjectPO; import org.eclipse.jubula.client.core.model.IReusedProjectPO; import org.eclipse.jubula.client.core.model.ITestSuitePO; import org.eclipse.jubula.client.core.model.PoMaker; import org.eclipse.jubula.client.core.persistence.CompNamePM; import org.eclipse.jubula.client.core.persistence.CompNamePM.SaveCompNamesData; import org.eclipse.jubula.client.core.persistence.GeneralStorage; import org.eclipse.jubula.client.core.persistence.PersistenceUtil; import org.eclipse.jubula.client.core.persistence.Persistor; import org.eclipse.jubula.client.core.persistence.ProjectPM; import org.eclipse.jubula.client.core.utils.TreeTraverser; import org.eclipse.jubula.client.core.persistence.PMException; import org.eclipse.jubula.tools.internal.exception.ProjectDeletedException; import org.eclipse.jubula.tools.internal.exception.JBException; /** * * The object managing Component Names * * The ComponentNamePOs are managed by the main session * After any changes made to component names, the committer must notify this Manager * So that the Manager can refresh the stored Component Names * Of course only CNs belonging to the current project can change, CNs belonging to reused projects never change * * Some of the functionality of this class comes from the ComponentNamesBP class * * @author BREDEX GmbH * @created 15.Jul, 2016 */ public class CompNameManager implements IComponentNameCache { /** i18n key for the "unknown" component type */ public static final String UNKNOWN_COMPONENT_TYPE = "guidancer.abstract.Unknown"; //$NON-NLS-1$ /** Indicates that a logical name is used in a reused project */ private static final Integer IN_REUSED = -2; /** Indicates that a logical name is used in the current project, and is saved to the DB */ private static final Integer IN_CURRENT = -1; /** The singleton instance */ private static CompNameManager instance = null; /** the currently open project (if there is any) - to avoid repetitive calling of the GeneralStorage */ private IProjectPO m_project; /** Managed Component Names by their GUIDs */ private HashMap<String, IComponentNamePO> m_compNames; /** map: guid => number of in-DB usage of Component Name */ private MapCounter m_usage; /** map: logical names => place of usage: current project or reused project */ // This is used to keep track of not-yet-saved component names private HashMap<String, Integer> m_logicalNames; /** All CN type problems */ private Map<String, ProblemType> m_allTypeProblems; /** * Singleton Constructor. */ private CompNameManager() { clear(); } /** * @return The singleton instance. */ public static final CompNameManager getInstance() { if (instance == null) { instance = new CompNameManager(); } return instance; } /** * Clears all Component Names */ public void clear() { m_compNames = new HashMap<String, IComponentNamePO>(20); m_usage = new MapCounter(); m_logicalNames = new HashMap<String, Integer>(20); m_project = GeneralStorage.getInstance().getProject(); } /** * reads all paramNames of the current Project and its reused Projects * from database into names map * @throws PMException in case of an (unexpected) DB access problem */ public final void init() throws PMException { clear(); initCompNamesTransitive(m_project.getId(), new HashSet<Long>(5)); countUsage(); recalculateTypes(); } /** * reads all Component Names of the given Project and its reused Projects * from the database * @param projectID an IProjectPO id * @param loadedProjectIds Accumulated IDs of reused projects that have * been loaded. * @throws PMException in case of an (unexpected) DB access problem */ private void initCompNamesTransitive(Long projectID, Set<Long> loadedProjectIds) throws PMException { if (projectID == null) { return; } readCompNamesForProjectID(projectID); for (IReusedProjectPO usedProj : ProjectPM .getReusedProjectsForProject(projectID)) { final String reusedGuid = usedProj.getProjectGuid(); final Integer reuseMajVers = usedProj.getMajorNumber(); final Integer reuseMinVers = usedProj.getMinorNumber(); final Integer reuseMicVers = usedProj.getMicroNumber(); final String reuseQualVers = usedProj.getVersionQualifier(); try { final Long usedProjId = ProjectPM .findProjectIDByGuidAndVersion(reusedGuid, reuseMajVers, reuseMinVers, reuseMicVers, reuseQualVers); if (usedProjId != null && loadedProjectIds.add(usedProjId)) { initCompNamesTransitive(usedProjId, loadedProjectIds); } } catch (JBException e) { // Continue! Maybe the Project is not present in DB. } } } /** * Refreshes Component Names of a given reused project * @param projectId the ID of the project * @throws PMException */ public void refreshNames(Long projectId) throws PMException { readCompNamesForProjectID(projectId); } /** * reads all paramNames of the Project with the given ID from database * into names map * @param projId the Project ID. * @throws PMException PMException in case of any db problem */ private void readCompNamesForProjectID(Long projId) throws PMException { List<IComponentNamePO> names = CompNamePM.readAllCompNamesRO(projId); boolean currProj = false; if (m_project != null) { currProj = projId.equals(m_project.getId()); } Integer pos = currProj ? IN_CURRENT : IN_REUSED; String name; for (IComponentNamePO compNamePO : names) { name = compNamePO.getName(); m_compNames.put(compNamePO.getGuid(), compNamePO); if (!m_logicalNames.containsKey(name) || m_logicalNames.get(name).equals(IN_CURRENT)) { m_logicalNames.put(name, pos); } } } /** * Determines for each Component Name how many times it is used in the current project */ public void countUsage() { m_usage.clear(); CountCompNameUsage op = new CountCompNameUsage(m_usage); TreeTraverser traverser = new TreeTraverser(m_project, op, true, false); traverser.setTraverseReused(false); traverser.traverse(true); for (ITestSuitePO ts : TestSuiteBP.getListOfTestSuites(m_project)) { traverser = new TreeTraverser(ts, op); traverser.traverse(true); } countCompNamesUsedInAssocs(); } /** * Returns the array of used Component Names * @param projId The id of the project * @return the used Component Names */ public Object[] getUsedCompNames(Long projId) { ArrayList<IComponentNamePO> res = new ArrayList<>(m_compNames.size()); for (IComponentNamePO cN : m_compNames.values()) { if (cN.getParentProjectId().equals(projId) && cN.getReferencedGuid() == null && m_usage.get(cN.getGuid()) != null && m_usage.get(cN.getGuid()) > 0) { res.add(cN); } } return res.toArray(); } /** * Counts all Component Name usages in associations */ private void countCompNamesUsedInAssocs() { Collection<IAUTMainPO> auts = m_project.getAutMainList(); String resGuid; Integer count; for (IAUTMainPO aut : auts) { for (IObjectMappingAssoziationPO assoc : aut.getObjMap().getMappings()) { if (assoc.getTechnicalName() != null) { for (String guid : assoc.getLogicalNames()) { resGuid = resolveGuid(guid); count = m_usage.get(resGuid); if (count == null) { count = 0; } m_usage.put(resGuid, count + 1); } } } } } /** * Returns the array of unused Component Names * @param projId The Project Id * @return the unused Component Names */ public Object[] getUnusedCompNames(Long projId) { ArrayList<IComponentNamePO> res = new ArrayList<>(m_compNames.size()); for (IComponentNamePO cN : m_compNames.values()) { if (cN.getReferencedGuid() == null && cN.getParentProjectId().equals(projId) && (m_usage.get(cN.getGuid()) == null || m_usage.get(cN.getGuid()).equals(0))) { res.add(cN); } } return res.toArray(); } /* * Backward compatibility methods */ /** {@inheritDoc} */ public String getNameByGuid(String guid) { IComponentNamePO compNamePo = getResCompNamePOByGuid(guid); if (compNamePo != null) { return compNamePo.getName(); } return guid; } /** {@inheritDoc} */ public String getGuidForName(String name) { for (IComponentNamePO cN : m_compNames.values()) { if (cN.getName().equals(name)) { return resolveGuid(cN.getGuid()); } } return null; } /** {@inheritDoc} */ public String getGuidForName(String name, Long parentProjectId) { for (IComponentNamePO cN : m_compNames.values()) { if (cN.getName().equals(name) && cN.getParentProjectId().equals(parentProjectId)) { return cN.getGuid(); } } return null; } /** * New methods */ /** * Returns the resolved ComponentNamePO identified by the guid * No need to provide ProjectId, because CNPOs are uniquely identified by their guids within a running Jubula * @param guid the guid * @return the ComponentNamePO */ public IComponentNamePO getResCompNamePOByGuid(String guid) { return m_compNames.get(resolveGuid(guid)); } /** * Decides whether the logical name is used anywhere * (either in the project, its reused projects or in an editor) * @param name the name * @return whether the logical name is used anywhere */ public boolean isLogNameUsed(String name) { return m_logicalNames.containsKey(name); } /** * Creates a Component Name with the given parameters * Should only be called if there is no Component Name with the given guid in the DB * @param guid the guid * @param name the name * @param type the type * @param ctx the creation context * @return the new Component Name */ public IComponentNamePO createCompNamePO(String guid, String name, String type, CompNameCreationContext ctx) { IComponentNamePO cN = PoMaker.createComponentNamePO(guid, name, type, ctx, m_project.getId()); return cN; } /** * Creates a new Component Name and persists it to the DB * @param name the name of the Component Name * @param type the type of the Component Name * @param ctx the creation context * @return the ComponentNamePO or null */ public IComponentNamePO createAndPersistCompNamePO(String name, String type, CompNameCreationContext ctx) throws PMException, ProjectDeletedException { // no comp name changed event is sent out, because the editors do not care about comp names created through the Browser IComponentNamePO cN = null; EntityManager sess = null; String guid = PersistenceUtil.generateUUID(); try { cN = PoMaker.createComponentNamePO(guid, name, type, ctx, m_project.getId()); sess = Persistor.instance().openSession(); EntityTransaction tx = Persistor.instance().getTransaction(sess); sess.persist(cN); Persistor.instance().commitTransaction(sess, tx); } finally { Persistor.instance().dropSession(sess); } cN = GeneralStorage.getInstance().getMasterSession().merge(cN); cN.setComponentType(type); m_compNames.put(guid, cN); m_logicalNames.put(name, IN_CURRENT); return cN; } /** * Renames the given Component Name * @param toRename the ComponentNamePO * @param newName the new name */ public void renameCompName(IComponentNamePO toRename, String newName) throws PMException, ProjectDeletedException { String guid = toRename.getGuid(); String oldName = toRename.getName(); IComponentNamePO cN = getResCompNamePOByGuid(guid); if (cN != null) { cN = PoMaker.cloneCompName(cN); EntityManager sess = null; try { sess = Persistor.instance().openSession(); EntityTransaction tx = Persistor.instance(). getTransaction(sess); cN.setName(newName); sess.merge(cN); Persistor.instance().commitTransaction(sess, tx); } finally { Persistor.instance().dropSession(sess); } GeneralStorage.getInstance().getMasterSession(). refresh(getResCompNamePOByGuid(guid)); } Integer pos = m_logicalNames.get(oldName); if (!pos.equals(IN_REUSED)) { m_logicalNames.remove(oldName); m_logicalNames.put(newName, pos); } else { m_logicalNames.put(newName, IN_CURRENT); } DataEventDispatcher.getInstance().fireDataChangedListener( toRename, DataState.Renamed, UpdateState.all); } /** * Deletes the managed Component Names * @param toDel the Comp Names to delete * @throws PMException * @throws ProjectDeletedException */ public void deleteCompNames(Set<IComponentNamePO> toDel) throws PMException, ProjectDeletedException { // is only called by the Browser, which contains managed Component Names EntityManager s = null; ArrayList<IComponentNamePO> deleted = new ArrayList<>(toDel.size()); try { s = Persistor.instance().openSession(); EntityTransaction tx = Persistor.instance() .getTransaction(s); Persistor.instance().lockPOSet(s, toDel); for (IComponentNamePO compName : toDel) { // safer to check if another user started to use the CN if (CompNamePM.getNumberOfUsages(s, m_project.getId(), compName.getGuid()) == 0) { deleted.add(compName); s.remove(s.merge(compName)); } } Persistor.instance().commitTransaction(s, tx); } finally { Persistor.instance().dropSession(s); } EntityManager main = GeneralStorage.getInstance().getMasterSession(); IComponentNamePO comp; Integer num; ArrayList<DataChangedEvent> events = new ArrayList<>(deleted.size()); for (IComponentNamePO compName : deleted) { m_compNames.remove(compName.getGuid()); m_usage.getCounter().remove(compName.getGuid()); num = m_logicalNames.get(compName.getName()); if (num != null && !num.equals(IN_REUSED)) { m_logicalNames.remove(compName.getName()); } main.detach(compName); events.add(new DataChangedEvent(compName, DataState.Deleted, UpdateState.all)); } DataEventDispatcher.getInstance().fireDataChangedListener( events.toArray(new DataChangedEvent[0])); } /** * Should be called every time right after a successful commit changing Component Names * @param saveData the data of the save */ public void compNamesChanged(SaveCompNamesData saveData) { List<IComponentNamePO> changes = saveData.getDBVersions(); // Refreshing the managed Component Names IComponentNamePO cN; String guid; String name; Integer use; EntityManager sess = GeneralStorage.getInstance().getMasterSession(); for (IComponentNamePO changed : changes) { guid = changed.getGuid(); cN = m_compNames.get(guid); if (cN == null) { cN = sess.find(changed.getClass(), changed.getId()); m_compNames.put(guid, cN); if (m_logicalNames.get(cN.getName()) == null) { m_logicalNames.put(cN.getName(), IN_CURRENT); } } else { name = cN.getName(); sess.refresh(cN); if (!name.equals(cN.getName())) { // the CN is renamed - we have to update our logical names Integer pos = m_logicalNames.get(name); if (pos != null && pos.equals(IN_CURRENT)) { m_logicalNames.remove(name); } pos = m_logicalNames.get(cN.getName()); if (pos == null) { m_logicalNames.put(cN.getName(), IN_CURRENT); } // we let all the editors know that the CN was renamed DataEventDispatcher.getInstance().fireDataChangedListener( cN, DataState.Renamed, UpdateState.all); } } } countUsage(); recalculateTypes(); } /** * Returns the resolved version of the guid * @param guid the guid to resolve * @return the resolved guid */ public String resolveGuid(String guid) { if (guid == null) { return guid; } String curr = guid; IComponentNamePO currPO = m_compNames.get(guid); // Hopefully there are no chains with length over 1000... int num = 0; while (currPO != null && currPO.getReferencedGuid() != null && num < 1000) { curr = currPO.getReferencedGuid(); currPO = m_compNames.get(guid); num++; } return curr; } /** * Returns the resolved logical name of a Component Name * @param guid the guid of the Component Name * @return the resolved logical name or the guid if Component Name does not exist */ public String getResLogNameByGuid(String guid) { IComponentNamePO cN = getResCompNamePOByGuid(guid); if (cN == null) { return guid; } return cN.getName(); } /** {@inheritDoc} */ public void updateStandardMapperAndCleanup(Long projectId) { // Do nothing } /** * Returns the usage counter for a Component Name * @param guid the guid * @return the usage counter */ public int getUsageByGuid(String guid) { Integer res = m_usage.get(guid); if (res == null) { res = 0; } return res; } /** {@inheritDoc} */ public Collection<IComponentNamePO> getAllCompNamePOs() { return m_compNames.values(); } /** * Returns the map of all problem types; * @return the problem types */ public Map<String, ProblemType> getTypeProblems() { return m_allTypeProblems; } /** * Recalculates all CN types */ public void recalculateTypes() { String mostAbstract = CompNameTypeManager.getMostAbstractType(); for (IComponentNamePO cN : CompNameManager.getInstance() .getAllCompNamePOs()) { if (GeneralStorage.getInstance().getProject().getId().equals( cN.getParentProjectId())) { cN.setComponentType(mostAbstract); cN.setTypeProblem(null); } } CalcTypes calc = new CalcTypes(CompNameManager.getInstance(), null); calc.setWriteTypes(true); calc.calculateTypes(); CompNameManager.getInstance().setTypeProblems(calc); } /** * Stores the type problems * @param calc the calculator used to calculate types */ public void setTypeProblems(CalcTypes calc) { m_allTypeProblems = calc.getAllProblems(); } /** {@inheritDoc} */ public Map<String, IComponentNamePO> getLocalChanges() { return new HashMap<>(); } }