Saturday, February 19, 2011

Configure Alfresco to send Mails with attachments

Well This is actually a simple thing to achieve, well actually "simple" when you got to see it working once, specially because although JavaMail itself is pretty straight forward, it can be a pain to get the whole thing done in practice with alfresco...just some configurations issues actually...enjoy.


CustomMailActionExecuter

We need to extend the default MailActionExecuter code provided in alfresco since the attachments are not handled in the code.

In your CustomMailActionExecuter add the PARAM_ATTACHEMENTS attachment action parameter parameter as shown in the code extract below. Note I simplified the code to send the actionedUponNodeRef itself as attachment, using the PARAM_ATTACHEMENTS modify the code to attach any amount of documents you need.


 public class CustomMailActionExecuter extends ActionExecuterAbstractBase  
implements InitializingBean
{
private static Log logger = LogFactory.getLog(CustomMailActionExecuter.class);
/**
* Action executor constants
*/
public static final String NAME = "abimail";
public static final String PARAM_TO = "to";
public static final String PARAM_TO_MANY = "to_many";
public static final String PARAM_SUBJECT = "subject";
public static final String PARAM_TEXT = "text";
public static final String PARAM_FROM = "from";
public static final String PARAM_TEMPLATE = "template";
public static final String PARAM_ATTACHEMENTS = "attachements";
/**
* From address
*/
private static final String FROM_ADDRESS = "alfresco@alfresco.org";
private static final String REPO_REMOTE_URL = "http://localhost:8080/alfresco";
/**
* The java mail sender
*/
private JavaMailSender javaMailSender;
/**
...



Now Let's modify the prepare method of the MimeMessagePreparator as such

 public void prepare(MimeMessage mimeMessage) throws MessagingException  
{
if (logger.isDebugEnabled())
{
logger.debug(ruleAction.getParameterValues());
}
MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
// set header encoding if one has been supplied
if (headerEncoding != null && headerEncoding.length() != 0)
{
mimeMessage.setHeader("Content-Transfer-Encoding", headerEncoding);
}
....
String text = null;
String templateClassPath = (String) ruleAction.getParameterValue(PARAM_TEMPLATE);
if(templateClassPath != null ){
String comment = (String)ruleAction.getParameterValue(PARAM_TEXT);
Map<String, Object> model = createEmailTemplateModel(actionedUponNodeRef, comment);
text = templateService.processTemplate("freemarker", templateClassPath , model);
}
// set the text body of the message
if (text == null)
{
text = (String)ruleAction.getParameterValue(PARAM_TEXT);
}
MimeMultipart content = new MimeMultipart("mixed");
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(text);
content.addBodyPart(textPart);
MimeBodyPart attachment = new MimeBodyPart();
attachment.setDataHandler(new DataHandler(new DataSource() {
public InputStream getInputStream() throws IOException {
ContentReader reader = serviceRegistry.getContentService().getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT);
return reader.getContentInputStream();
}
public OutputStream getOutputStream() throws IOException {
throw new IOException("Read-only data");
}
public String getContentType() {
return serviceRegistry.getContentService().getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT).getMimetype();
}
public String getName() {
return nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_NAME).toString();
}
}));
content.addBodyPart(attachment);
mimeMessage.setContent(content);
// set the from address
NodeRef person = personService.getPerson(authService.getCurrentUserName());
String fromActualUser = null;
if (person != null)
{
fromActualUser = (String) nodeService.getProperty(person, ContentModel.PROP_EMAIL);
}
if( fromActualUser != null && fromActualUser.length() != 0)
{
message.setFrom(fromActualUser);
}
else
{
...
}
}
};
try
{
// Send the message unless we are in "testMode"
if(!testMode)
{
javaMailSender.send(mailPreparer);
}
else
{
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
mailPreparer.prepare(mimeMessage);
lastTestMessage = mimeMessage;
} catch(Exception e) {
System.err.println(e);
}
}
}
catch (MailException e)
{
String to = (String)ruleAction.getParameterValue(PARAM_TO);
if (to == null)
{
Object obj = ruleAction.getParameterValue(PARAM_TO_MANY);
if (obj != null)
{
to = obj.toString();
}
}
logger.error("Failed to send email to " + to, e);
throw new AlfrescoRuntimeException("Failed to send email to:" + to, e);
}
}



You need to be carefull here with two things.

1 - Create the content of the mail as a MimeMultipart and inititize the default content encoding as "mixed".
2 - Create two MimeBodyPart one for holding the text and another one that will hold the attachment. Set the Attachment MimeBodyPart content handler by using its method "setDataHandler" initialized with a an implementation of the "DataSource" Interface that actually reads the repository node reference content property, the content mimetype and its name.

That's all folks!!!

bonus: For those who are interested in creating their own Email template. Just very easy too.

The templateService "processTemplate" method is able to read template definition not only from the repository but also from a node in the class path. So simply create your templates in "/tomcat/shared/classes/extention/mails" for instance and acces the template by using something similar to this.


 ...  
String templateClassPath = (String) ruleAction.getParameterValue(PARAM_TEMPLATE);
if(templateClassPath != null ){
String comment = (String)ruleAction.getParameterValue(PARAM_TEXT);
Map<String, Object> model = createEmailTemplateModel(actionedUponNodeRef, comment);
text = templateService.processTemplate("freemarker", templateClassPath , model);
}
...


where here PARAM_TEMPLATE holds the class path of a template for instance

extension/mails/my-custom-mail-template.ftl

Do not forget to configure your CustomMailActionExecuter in the spring context. You might also need to add a "mailService" bean from the class "org.springframework.mail.javamail.JavaMailSenderImpl" to configure your internal javaMailSender.

Okay we are done here. Enjoy!!!

Friday, September 10, 2010

Custom endpoint for Alfresco remote access

It happen more than often that we want to perform actions in the repository that require custom authentication processes. Usually we need to define a This is a way to implement such a thing.

1. Customize webscripts-framework-config.xml (Alfresco <>or share-config-custom (Alfresco 3.2 >)

1.1. Define an Authenticator

 <authenticator> 
<id>alfresco-custom-ticket</id>
<name>Alfresco Authenticator</name>
<description>Alfresco Authenticator&lt;/description>
<class>com.westernacher.wps.share.authenticate.customAuthenticator</class>
</authenticator>

Where the customAuthenticator Class encloses the logic of our customized authentication.

1.2. Define a Connector

 <connector>  
<id>external-alfresco</id>
<name>Simple Http Connector</name>
<description>Simple Alfresco - HTTP Connector</description>
<class>org.alfresco.connector.AlfrescoConnector</class>
<authenticator-id>alfresco-custom-ticket</authenticator-id>
</connector>
Its however important to remind that in alfresco 3.2 > the classes for the connector were mostly ported to the spring framework package.

example:
the class org.alfresco.connector.AlfrescoConnector should be replaced with org.springframework.extensions.webscripts.connector.AlfrescoConnector in alfresco 3.2 and more.

1.3. Define the endpoint

 <endpoint>
<id>my-custom-end-point</id>
<name>Custom Endpoint</name>
<description>System account access to Alfresco</description>
<connector-id>http</connector-id>
<endpoint-url>url-tha-prefixes-the-scripts-to-be-called</endpoint-url>
<unsecure>true</unsecure>
</endpoint>

In alfresco by example the end point URL is standardly known as : http://localhost:8080/alfresco/s

1.4. Implement our CustomAuthenticator class


 public class CustomAuthenticator extends AbstractAuthenticator { 
public final static String CS_PARAM_ALF_TICKET = "alfTicket";
private static
Log logger = LogFactory.getLog(CustomAuthenticator.class);
@Override
public ConnectorSession authenticate(String endpoint, Credentials credentials, ConnectorSession connectorSession) throws AuthenticationException {
ConnectorSession cs = null;
if (credentials != null) {
//get a valid ticket from Alfresco via a standard or custom method
final
String url = endpoint + "/getTicket";
GetMethod get = new GetMethod(url);
// username and coockie
String aCoockie = (String) credentials.getProperty(Credentials.CREDENTIAL_PASSWORD);
get.setRequestHeader("Cookie", aCoockie);
HttpClient client = new HttpClient();
try {
int res = client.executeMethod(get);
// read back the ticket
if (res == 200) {
String ticket;
try {
ticket = get.getResponseBodyAsString();
} catch (
Throwable err) {
logger.error("Error to obtain ticket for custom authorization", err);
// the ticket that came back could not be parsed
// this will cause the entire handshake to fail

throw new
AuthenticationException("Unable to retrieve login ticket from Alfresco");
}
if (logger.isDebugEnabled()) {
logger.debug("
Parsed ticket: " + ticket);
}
// place the ticket back into the connector session
if (connectorSession != null) {
connectorSession.setParameter(
CS_PARAM_ALF_TICKET, ticket);
// signal that this succeeded
cs = connectorSession;
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Authentication failed, received response code: " + res);
}
}
} catch (
Throwable e) {
logger.error("
Error to authenticate by custom Authentication", e);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("
Authentication failed, no credentials provided");
}
}
return cs;
}
@Override
public
boolean isAuthenticated(String endpoint, ConnectorSession connectorSession) {
return (connectorSession.getParameter(
CS_PARAM_ALF_TICKET) != null);
}
}





Tuesday, August 31, 2010

Create A transaction supported cron Job in Alfresco

1. spring bean configuration

<bean id="customService" class="com.alfresco.ibrahim.services.impl.CustomServiceImpl">
<property name="customBeanDAO">
<ref bean="customBeanDAO" />
</property>
<property name="nodeService">
<ref bean="nodeService" />
</property>
<property name="searchService">
<ref bean="searchService" />
</property>
</bean>

2. cron config bean

<bean id="customGenerator" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass" value="com.alfresco.ibrahim.jobs.impl.customExecutorImpl"/>
<property name="jobDataAsMap">
<map>
<entry key="customService">
<ref bean="customService" />
</entry>
<entry key="transactionService">
<ref bean="transactionService" />
</entry>
</map>
</property>
</bean>

<bean id="regularUpdateTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="dailyReportGenerator"/>
<property name="cronExpression" value="30 * * * * ?"/>
</bean>

3. Job bean class


public class CustomExecutorImpl extends QuartzJobBean implements ReportExecutorJob{

private CustomService customService;
private TransactionService transactionService;


@Override
public void executeReportGeneration() {

AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Void>() {

@Override
public Void doWork() throws Exception {

transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<Void>() {

@Override
public Void execute() throws Throwable {
customService.doSomeJob();
return null;
}
}, false, true);

return null;

}
},AuthenticationUtil.getAdminUserName());
}

Sunday, March 28, 2010

Perform repository actions as administrator in a transaction

I have faced the need to run a code as admin several times and every time i needed to look for how this was done correctly. Let's put a stick on my wall :-)


you will need:
  1. An AuthenticationUtil for handling the Authentication as and access of the method as a given user: runAs.
  2. A RetryingTransactionHelper to handle transactions requirements
  3. optionally I use here the synchronized modificator for the specific need of this example.

public synchronized Long getNextContractNumber() {
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Long>() {
public Long doWork() throws Exception {
Long actualValue= null;
RetryingTransactionCallback<Long> transaction = new RetryingTransactionCallback<Long>() {
public Long execute() throws Exception {

List<String> currentNumberPathNames = getPathNames(currentNumberNodePath);
final NodeRef currentNumberNode = getNodeFromPathNames(currentNumberPathNames);
Long currentValue = (Long) nodeService.getProperty(currentNumberNode, PhagModel.PROP_CURRENT_NUMBER);
Long nextValue = currentValue + 1 ;
nodeService.setProperty(currentNumberNode, PhagModel.PROP_CURRENT_NUMBER, nextValue);
return nextValue;
}
};
actualValue = getTransactionHelper().doInTransaction(transaction);
return actualValue;
}
}, AuthenticationUtil.getAdminUserName());
}
it is a useful tool, it might certainly be useful for many.

Monday, February 8, 2010

ALfresco 3.1 Sending Ajax Request to Backend Webscript

To Send a Backend Ajax request in Alfresco you will need few things.
  1. The URL of the webscript you are invoking.
  2. The Object to be send in the request body if the method used is POST
  3. The Format of the Data to be Send if the method to be used is POST (optional).
  4. The success callback function
  5. The failure callback function
Prepare the URL to be called:

var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "api/oase-metadata/node/{nodeRef}",
{
nodeRef: file.nodeRef.replace(":/", "")
});


Using the substitute method of the Yui framework, the URL is built up using the values passed in the JSON Object.

Prepare the success and failure callback function:

This method are used to handle the AJAX request return results. If everything goes fine the success handler is executed at the end of the AJAX request, otherwise the failure handler is called.


...
,

onSuccess: function _SuccessNotify(){


Alfresco.util.PopupManager.displayMessage( {
text : "Metadata Updated Sucessfully"
});
YAHOO.lang.later(2000,this,function(){
window.location.reload(true);
});
},
onFailure: function _FailureNotify(){


Alfresco.util.PopupManager.displayMessage( {
text : "Metadata Updated Failure"
});
}
,
...



Implement the Ajax request using the previously defined assets:

Some additional parameters are needed.

RequestContentType: is Optional but indicates here that the Object passed as parameter o the request has to be transmitted as a JSON object, not as JavaScript Native Object.

Method: Specifies the HTTP method used for processing the request. here POST is used so the data wll be transmitted in the body of a HTTP-POST Request.

DataObj: is the Object to be transmitted to the server. Its construction is similar to a JSON object definition, and usinh the requestContentType formatter value can also be sent as such.

SuccessCallback and failureCallback fields define the success an failure handlers for this AJAX request.



Alfresco.util.Ajax.request(
{
url: actionUrl,
requestContentType: Alfresco.util.Ajax.JSON,
method: Alfresco.util.Ajax.POST,
dataObj:
{
htmlid: this.id,

dataObject:
{
dateofdoc : dateofdoc,
dateofrec : dateofrec,
subjectofdoc : subjectofdoc,
typeofdoc : typeofdoc,
sendername : sendername,
senderstreet : senderstreet,
senderpostcode : senderpostcode,
sendercity: sendercity
}
},
successCallback:
{
fn: this.onSuccess,
scope: this
},
failureCallback:
{
fn: this.onFailure,
scope: this
},
execScripts: true,
});
...
},

Sunday, February 7, 2010

Alfresco 3.1 CIFS Quick Configuration.

Short and Effective!!!

In your alfresco installation directory locate
file-server-custom.xml.sample.
It is encouraged to changes configuration files of Alfresco only in the shared directory so:
In the
/tomcat/shared/classes/alfresco/extension directory locate the above named file.


The content looks like this:

 <alfresco-config area="file-servers">
<!-- To override the default Alfresco filesystem use replace="true", to -->
<!-- add additional filesystems remove the replace="true" attribute -->
<config evaluator="string-compare" condition="Filesystems" replace="true">
<filesystems>
<!-- Alfresco repository access shared filesystem -->
<filesystem name="${filesystem.name}">
<store>workspace://SpacesStore</store>
<rootPath>/app:company_home</rootPath>
<!-- Add a URL file to each folder that links back to the web client -->
<urlFile>
<filename>__Alfresco.url</filename>
<webpath>http://${localname}:8080/alfresco/</webpath>
</urlFile>
<!-- Mark locked files as offline -->
<offlineFiles/>
...
</filesystem>
<!-- AVM virtualization view of all stores/versions for WCM -->
<!-- virtual view can be any of the following: normal, site, staging, author, preview -->
<avmfilesystem name="AVM">
<virtualView stores="site,staging,author"/>
</avmfilesystem>
</filesystems>
</config>
</alfresco-config>


In the configuration file, filesystem.name refers to the name of the file system you are creating and localname refers to the name of you user to access your server via a browser URL. To avoid using localhost you might consider creating a new entry in the host configuration file in windows: c:\windows\system32\drivers\etc\ directory.

Rename the file-server-custom.xml.sample to
file-server-custom.xml
Change the values
${filesystem.name} and ${server.local.name} accordingly. In my case i used alfresco and cifs4real.com respectively (the latest one configured in the host file and bound to my network IP address).

Save and close the
file-server-custom.xml file in the same folder as the sample. That means in the extension folder.

Now before strarting you alfreso server make sure the port 445 is not in use. To do it just run the command netstats -a from a command prompt. If it appears that the port is in used turn off the service by sending net stop SERVICE_NAME from the command prompt. Do the same with start instead of stop when you are done with this exercise if you need to start the service again. We are almost done here.

First Run the command.
nbtstat -a SERVER_IP_ADDRESS

Now start the alfresco server.

Now we need to map a space of Alfresco as a device in your local machine. Do this using for instance the command.
net use X: \\SERVER_NAMEa\Alfresco\SPACE_NAME /user:ALFRESCO_USERNAME

The "a" at the end of your machine SERVER_NAME is not optional. The space name can be for instance "Sites".


That's all, now you can access your mounted alfresco space in the windows explorer.