package com.almworks.jira.structure.api.sync;
import com.almworks.jira.structure.api.*;
import com.almworks.jira.structure.api.forest.Forest;
import com.almworks.jira.structure.util.StructureUtil;
import com.almworks.jira.structure.util.SyncLogger;
import com.atlassian.annotations.PublicSpi;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.issue.MutableIssue;
import com.atlassian.jira.security.JiraAuthenticationContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.*;
import java.io.*;
import java.util.Map;
/**
* <p><code>AbstractSynchronizer</code> is an abstract base class for the synchronizers that
* provides basic implementation for some of the {@link StructureSynchronizer} methods
* and offers some utility methods for the synchronizers.</p>
*
* <p>If your synchronizer needs to listen to JIRA issue events, you probably need
* to extend {@link AbstractIssueListeningSynchronizer} instead of this class.</p>
*
* <p>The type parameter for this class is the actual type of the parameters used
* by this sycnhronizer. The basic implementation of the {@link #storeParameters}
* and {@link #restoreParameters} methods serialize and deserialize <code>P</code>
* using JAXB, expecting that the parameters type are properly annotated (see
* {@link javax.xml.bind.annotation.XmlRootElement}).</p>
*
* @author Igor Sereda
*/
@PublicSpi
public abstract class AbstractSynchronizer<P> implements StructureSynchronizer {
private static final Logger logger = LoggerFactory.getLogger(AbstractSynchronizer.class);
protected final StructureServices myStructureServices;
protected final StructureManager myStructureManager;
protected final JiraAuthenticationContext myAuthenticationContext;
protected final IssueManager myIssueManager;
protected final SynchronizerUndoRecorder myUndoRecorder;
private StructureSynchronizerModuleDescriptor myDescriptor;
private final Class<P> myParametersClass;
/**
* Constructs an instance of the synchronizer.
*
* @param structureServices services facade
* @param parametersClass parameters class
*/
protected AbstractSynchronizer(StructureServices structureServices, Class<P> parametersClass) {
myStructureServices = structureServices;
myParametersClass = parametersClass;
myStructureManager = structureServices.getStructureManager();
myAuthenticationContext = structureServices.getAuthenticationContext();
myIssueManager = structureServices.getIssueManager();
myUndoRecorder = structureServices.getSynchronizerUndoRecorder();
}
/**
* @return the expected class of the parameters
*/
public Class<P> getParametersClass() {
return myParametersClass;
}
/**
* Called by the module descriptor on initialization
*
* @param descriptor descriptor for this module
*/
public synchronized void init(StructureSynchronizerModuleDescriptor descriptor) {
myDescriptor = descriptor;
}
/**
* @return module descriptor, which can be used to retrieve configuration for this synchronizer
* from the atlassian-plugin.xml
*/
@NotNull
public synchronized StructureSynchronizerModuleDescriptor getDescriptor() {
return myDescriptor;
}
/**
* Looks up i18n text using the i18n bean from the module's plugin and the current
* user's locale.
*
* @param key text key
* @param parameters optional parameters
* @return the text or the key, if not found
*/
@NotNull
protected String getText(@NotNull String key, Object... parameters) {
return getDescriptor().getI18nBean().getText(key, parameters);
}
/**
* <p>Casts the parameters object passed from outside to the expected parameters class.</p>
*
* <p>If object class does not match, logs a warning and returns null.</p>
*
* @param p the parameters object
* @return <code>p</code> cast to <code>P</code>
*/
@Nullable
protected P castParameters(Object p) {
if (p == null) return null;
if (!myParametersClass.isInstance(p)) {
logger.warn(this + ": params of class " + p.getClass().getName() + " are not acceptable");
return null;
}
return myParametersClass.cast(p);
}
public void start(@NotNull SyncInstance instance, @NotNull SyncController controller) {
}
public void stop(@NotNull SyncInstance instance) {
}
public void addDefaultFormParameters(@NotNull Map<String, Object> params) {
}
/**
* Returns the current forest of the structure that this sync instance refers.
*
* @param instance the sync instance
* @param log {@link SyncLogger logging helper} that will be used to log warning in case forest cannot be obtained.
* You can specify SyncLogger that you are already using in your synchronizer to write accurate sync mode to the logs (autosync or resync)
* @return the forest or <code>null</code> if the structure is not present or running user has no access to it
*/
@Nullable
protected Forest getSourceForest(@NotNull SyncInstance instance, @NotNull SyncLogger log) {
try {
return myStructureManager.getForest(instance.getStructureId(), getCurrentUser(), false);
} catch (StructureException e) {
if (e.getError() == StructureError.STRUCTURE_NOT_EXISTS_OR_NOT_ACCESSIBLE) log.warnStructureException(e);
else log.warnExceptionIfDebug(e, "could not run");
return null;
}
}
/**
* Returns the current forest of the structure that this sync instance refers.
*
* @param instance the sync instance
* @return the forest or <code>null</code> if the structure is not present or running user has no access to it
*/
@Nullable
protected Forest getSourceForest(@NotNull SyncInstance instance) {
return getSourceForest(instance, new SyncLogger(logger, instance, myStructureManager, false));
}
public byte[] storeParameters(Object parameters) throws IOException {
if (parameters == null) return null;
try {
JAXBContext jaxbContext = StructureUtil.createJAXBContext(myParametersClass);
if (jaxbContext == null) throw new IOException("cannot create JAXB context for " + myParametersClass);
Marshaller marshaller = jaxbContext.createMarshaller();
ByteArrayOutputStream out = new ByteArrayOutputStream();
// marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(parameters, out);
return out.toByteArray();
} catch (JAXBException e) {
logger.error("cannot serialize parameters", e);
throw new IOException("cannot serialize parameters " + parameters, e);
}
}
public P restoreParameters(byte[] data) throws IOException {
if (data == null) return null;
try {
JAXBContext jaxbContext = StructureUtil.createJAXBContext(myParametersClass);
if (jaxbContext == null) throw new IOException("cannot create JAXB context for " + myParametersClass);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
Object object = unmarshaller.unmarshal(new ByteArrayInputStream(data));
if (object == null) return null;
if (!myParametersClass.isInstance(object)) {
logger.warn("cannot unmarshal synchronizer parameters, unexpected class " + object.getClass());
return null;
}
return (P) object;
} catch (JAXBException e) {
throw new IOException("error unmarshalling parameters", e);
}
}
/**
* Checks that the user has at least {@link PermissionLevel#EDIT} permission on the specified structure.
*
* @param structureId the ID of the structure
* @return true if the current user is allowed to modify the structure
*/
protected boolean verifyStructureEditPermissions(long structureId) {
boolean r = myStructureManager.isAccessible(structureId, getCurrentUser(), PermissionLevel.EDIT, false);
if (!r) {
logger.warn("{} cannot run under user {} because he or she does not have permissions to edit structure {}", new Object[]{this, StructureUtil.username(getCurrentUser()), structureId});
}
return r;
}
/**
* Checks that the user has at least {@link PermissionLevel#EDIT} permission on the specified structure.
*
* @param structureId the ID of the structure
* @param log {@link SyncLogger logging helper} that will be used to log warning in case the structure does not exist or is not accessible;
* in case you don't need synchronizer information in the logs, you can use {@link #verifyStructureEditPermissions(long)}
* @return true if the current user is allowed to modify the structure
*/
protected boolean verifyStructureEditPermissions(long structureId, SyncLogger log) {
boolean r = myStructureManager.isAccessible(structureId, getCurrentUser(), PermissionLevel.EDIT, false);
if (!r) {
log.warn("cannot run under user", StructureUtil.username(getCurrentUser()), "because he or she does not have permissions to edit the structure");
}
return r;
}
private User getCurrentUser() {
return myAuthenticationContext.getLoggedInUser();
}
/**
* Retrieves an instance of <code>Issue</code>.
*
* @param issueId the ID of the issue
* @return the issue, or null if the issue cannot be found or there is an exception getting it
*/
@Nullable
protected MutableIssue getIssue(long issueId) {
MutableIssue issueObject = null;
try {
issueObject = myIssueManager.getIssueObject(issueId);
} catch (Exception e) {
logger.warn("cannot retrieve issue " + issueId + ": " + e);
}
return issueObject;
}
/**
* Retrieves an instance of issue by issue key.
*
* @param key issue key
* @return the issue, or null if the issue cannot be found or there is an exception getting it
*/
@Nullable
protected MutableIssue getIssue(@NotNull String key) {
MutableIssue issueObject = null;
try {
issueObject = myIssueManager.getIssueObject(key);
} catch (Exception e) {
logger.warn("cannot retrieve issue " + key + ": " + e);
}
return issueObject;
}
/**
* Returns a string representation of the issue that is used to write log messages. Writes issue ID and, if possible,
* issue key.
* @param issue the ID of the issue
* @return string that can be used in output
*
* @deprecated use {@link StructureUtil#getDebugIssueString(Long)}
*/
@Deprecated
@NotNull
public static String issueDebug(@Nullable Long issue) {
return StructureUtil.getDebugIssueString(issue);
}
}