[백업][가리사니] xml Rpc 블로그 : 5. java로 구현해보자
java, protocols, xml

이 문서는 가리사니 개발자 포럼에 올렸던 글의 백업 파일입니다. 오래된 문서가 많아 현재 상황과 맞지 않을 수 있습니다.

XML-RPC 블로그 시리즈

Apache XML-RPC 를 이용하는 방법

  • 사실 너무많은 파일이 필요해서 필자는 직접 구현을 더 추천합니다.
  • 직접구현은 아래에 있습니다.
  • 클라이언트 기능만 쓰지만, 서로 연관된것들이 많아 추가 후 필요없는 기능들을 제거하시면 될 것 같습니다. pom.xml 추가
<dependency>
    <groupId>org.apache.xmlrpc</groupId>
    <artifactId>xmlrpc-common</artifactId>
    <version>3.1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.xmlrpc</groupId>
    <artifactId>xmlrpc-client</artifactId>
    <version>3.1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.xmlrpc</groupId>
    <artifactId>xmlrpc-server</artifactId>
    <version>3.1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.ws.commons.util</groupId>
    <artifactId>ws-commons-util</artifactId>
    <version>1.0.2</version>
</dependency>
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.1</version>
</dependency>

글 등록 구현 예제

XmlRpcClient blog  = new XmlRpcClient();
XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
Hashtable<String, String> post = new Hashtable<>();
Vector<Serializable> params = new Vector<Serializable>();

// 예를들어 네이버 기준으로 https://api.blog.naver.com/xmlrpc
config.setServerURL(new URL("XML RPC 주소"));
config.setBasicEncoding("utf-8");
config.setEncoding("utf-8");

// 포스트 등록글 참고바람.
// 네이버 같은경우 블로그 ID나 서비스 ID나 그냥 ID 입니다.
params.addElement("블로그 ID");
params.addElement("서비스 ID");
params.addElement("API 키");

// 각종 컨텐츠 구조체(포스트 등록 참고)
post.put("title", "타이틀");
post.put("description", "컨텐츠<b>asdf</b>asdf");
post.put("tags", "가리사니,개발자,공간");
post.put("categories", "프로그래밍");
params.addElement(post);

// 숨김여부
params.addElement(true);

// 성공시 포스트 ID가 리턴됩니다.
// 실패시 Exception 으로 오류를 알려줍니다.
String blogPostID = (String) blog.execute(config, "metaWeblog.newPost", params);

직접구현

XML RPC 클라이언트 객체

  • 급하게 대충짠거라 소스가 엉망입니다. 하하하..;;
  • 인코딩은 [http://www.htmlescape.net/htmlescape_for_java.html 참고해보세요.
  • 다만 위 사이트 소스는 의도된건지 버그인지 모르겠지만 < 가 있으면 < -> &lt; 로 연속으로 분해됨으로,
  • & 를 먼저 처리하게 수정하시기 바랍니다. 그냥 이소스 그대로 쓰셔도됩니다....
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * XmlRpcClient<br>
 * 2016-07-03 전명 박용서 : 작성<br>
 * 급하게 짠거라 리팩토링해서 쓰세요..
 */
public class XmlRpcClient
{
	/** 커넥션 */
	private URLConnection connection;
	/** 요청 */
	private Writer request;

	/** 생성자 : newInstance 사용 */
	private XmlRpcClient() {}

	/**
	 * 연결을 합니다.<br>
	 * SSL/TLS 인증에 문제가 있는 경우에만 여기서 수동으로 사용하길 바라며,
	 * 그렇지않은경우 newInstance(URL url, String method) 를 사용하기 바랍니다.
	 * @param url
	 * @param sslSocketFactory
	 * @param method
	 * @return
	 * @throws IOException
	 */
	public static XmlRpcClient newInstance(URL url, SSLSocketFactory sslSocketFactory, String method)
		throws IOException
	{
		// 연결
		HttpURLConnection connection = (HttpURLConnection) url.openConnection();

		// 커스텀 보안인증
		if (sslSocketFactory != null && "https".equals(url.getProtocol()))
		{
			((HttpsURLConnection)connection).setSSLSocketFactory(sslSocketFactory);
		}

		connection.setRequestMethod("POST");
		connection.setDoOutput(true);
		connection.setDoInput(true);
		// 헤더들을 지워주지 않으면 서버에 따라서 인식을 못함.
		connection.setRequestProperty("User-Agent", "");
		connection.setRequestProperty("Accept", "");
		connection.setRequestProperty("Content-type", "");
		connection.connect();

		// 연결
		XmlRpcClient xmlRpcClient = new XmlRpcClient();
		xmlRpcClient.connection = connection;
		xmlRpcClient.request = new OutputStreamWriter(connection.getOutputStream(), "utf-8");

		// 기본문입력
		xmlRpcClient.request.write("<methodCall><methodName>");
		xmlRpcClient.request.write(method);
		xmlRpcClient.request.write("</methodName><params>");

		return xmlRpcClient;
	}

	/**
	 * 연결을 합니다.
	 * @param url
	 * @param method
	 * @return
	 * @throws IOException
	 */
	public static XmlRpcClient newInstance(URL url, String method)
		throws IOException
	{
		return newInstance(url, null, method);
	}

	/**
	 * XML 이스케이프
	 * @param out
	 * @param value
	 * @throws IOException
	 */
	private static void encodeXmlEscape(Writer out, String value) throws IOException
	{
		char[] chars = value.toCharArray();
		for (char c : chars)
		{
			switch (c)
			{
				case '&' : out.write("&amp;"); break;
				case '<' : out.write("&lt;"); break;
				case '>' : out.write("&gt;"); break;
				default : out.write(c);
			}
		}
	}

	/**
	 * 타입을 지정하는 파라미터를 추가합니다.
	 * @param type
	 * @param value
	 * @return
	 * @throws IOException
	 */
	public XmlRpcClient addParam(String type, String value) throws IOException
	{
		request.write("<param><value><");
		request.write(type);
		request.write('>');
		encodeXmlEscape(request, value);
		request.write("</");
		request.write(type);
		request.write("></value></param>");
		return this;
	}

	/**
	 * 파라미터를 추가합니다.<br>
	 * 별도 데이터 타입을 넣지 않습니다.
	 * @param value
	 * @return
	 * @throws IOException
	 */
	public XmlRpcClient addParam(String value) throws IOException
	{
		request.write("<param><value>");
		encodeXmlEscape(request, value);
		request.write("</value></param>");
		return this;
	}

	/**
	 * string 타입의 파라미터를 추가합니다.
	 * @param value
	 * @return
	 * @throws IOException
	 */
	public XmlRpcClient addParamString(String value) throws IOException
	{
		return addParam("string", value);
	}

	/**
	 * int 타입의 파라미터를 추가합니다.
	 * @param value
	 * @return
	 * @throws IOException
	 */
	public XmlRpcClient addParamInt(int value) throws IOException
	{
		return addParam("int", Integer.toString(value));
	}

	/**
	 * boolean 타입의 파라미터를 추가합니다.
	 * @param value
	 * @return
	 * @throws IOException
	 */
	public XmlRpcClient addParamBoolean(boolean value) throws IOException
	{
		return addParam("boolean", value ? "1" : "0");
	}

	/**
	 * struct 타입의 파라미터를 추가합니다.
	 * @param struct
	 * @return
	 * @throws IOException
	 */
	public XmlRpcClient addParamStruct(XmlRpcStruct struct) throws IOException
	{
		request.write("<param><value>");
		struct.out(request);
		request.write("</value></param>");
		return this;
	}

	/**
	 * 최종 결과를 가져옵니다.<br>
	 * <b>주의 : </b><br>
	 * XmlRpcClient 객체는 send 를 부름으로써 끝나게 됩니다.<br>
	 * 즉, 이 객체를 다시 재활용할 수 없습니다.<br>
	 * 새롭게 newInstance 하시기 바랍니다.
	 * @return
	 * @throws IOException
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 */
	public XmlRpcResult send() throws IOException, SAXException, ParserConfigurationException
	{
		// 보내기 끝 / 닫기
		request.write("</params></methodCall>");
		request.flush();
		request.close();

		// 반환
		try (InputStream is = connection.getInputStream())
		{
			return new XmlRpcResult(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is));
		}
		catch (IOException e)
		{
			throw e;
		}
	}

	/**
	 * XmlRpcStruct 객체
	 */
	public static class XmlRpcStruct
	{
		private Writer data = new StringWriter();

		/**
		 * 만들어진 결과를 wirter 를 통해 출력합니다.
		 * @param writer
		 * @throws IOException
		 */
		private void out(Writer out) throws IOException
		{
			out.write("<struct>");
			out.write(((StringWriter)data).toString());
			out.write("</struct>");
		}

		/**
		 * 맴버추가
		 * @param name
		 * @param type
		 * @param value
		 * @return
		 * @throws IOException
		 */
		public XmlRpcStruct addMember(String name, String type, String value) throws IOException
		{
			data.write("<member><name>");
			encodeXmlEscape(data, name);
			data.write("</name><value><");
			data.write(type);
			data.write('>');
			encodeXmlEscape(data, value);
			data.write("</");
			data.write(type);
			data.write("></value></member>");
			return this;
		}

		/**
		 * 맴버추가
		 * @param name
		 * @param value
		 * @return
		 * @throws IOException
		 */
		public XmlRpcStruct addMember(String name, String value) throws IOException
		{
			data.write("<member><name>");
			encodeXmlEscape(data, name);
			data.write("</name><value>");
			encodeXmlEscape(data, value);
			data.write("</value></member>");
			return this;
		}

		/**
		 * 맴버추가
		 * @param name
		 * @param type
		 * @param value
		 * @return
		 * @throws IOException
		 */
		public XmlRpcStruct addMemberArray(String name, String type, String... values) throws IOException
		{
			data.write("<member><name>");
			encodeXmlEscape(data, name);
			data.write("</name><value><array><data>");
			for (String value : values)
			{
				data.write("<value><");
				data.write(type);
				data.write('>');
				encodeXmlEscape(data, value);
				data.write("</");
				data.write(type);
				data.write("></value>");
			}
			data.write("</data></array></value></member>");
			return this;
		}

		/**
		 * 맴버추가
		 * @param name
		 * @param value
		 * @return
		 * @throws IOException
		 */
		public XmlRpcStruct addMemberString(String name, String value) throws IOException
		{
			return addMember(name, "string", value);
		}

		/**
		 * 맴버추가
		 * @param name
		 * @param value
		 * @return
		 * @throws IOException
		 */
		public XmlRpcStruct addMemberStringArray(String name, String... value) throws IOException
		{
			return addMemberArray(name, "string", value);
		}
	}

	/**
	 * XmlRpcResult
	 */
	public static class XmlRpcResult
	{
		private final Document dom;
		private final boolean success;
		private String result;
		private Map<String, String> fault;

		/** 직접 생성 불가 */
		private XmlRpcResult(Document dom)
		{
			this.dom = dom;

			// 성공
			if ( (this.success = (this.dom.getElementsByTagName("fault").getLength() == 0)) )
			{
				NodeList nl = this.dom.getElementsByTagName("value");
				if (nl.getLength() != 0)
				{
					Node node = nl.item(0);
					if (node.hasChildNodes())
					{
						result = node.getChildNodes().item(0).getTextContent();
					}
					else
					{
						result = node.getTextContent();
					}
				}
			}
			// 실패
			else
			{
				NodeList nl = this.dom.getElementsByTagName("member");
				fault = new HashMap<>();
				for (int i = 0 ; i < nl.getLength() ; i++)
				{
					Node node = nl.item(i);
					NodeList member = node.getChildNodes();
					String name = null, value = null;

					for (int j = 0 ; j < member.getLength() ; j++)
					{
						Node attr = member.item(j);
						switch (attr.getNodeName())
						{
							case "name" : name = attr.getTextContent(); break;
							case "value" : value = attr.getTextContent(); break;
						}
					}

					if (name != null && value != null)
					{
						fault.put(name.toLowerCase(), value);
					}
				}

			}
		}

		/**
		 * 최종 결과 xml을 가져옵니다.<br>
		 * 나중에 dom을 복사하지못하도록 방어적 복사복 추가 바람.
		 * @return
		 */
		public Document getResultDocument()
		{
			return this.dom;
		}

		/**
		 * 성공여부를 가져옵니다.
		 * @return
		 */
		public boolean isSuccess()
		{
			return success;
		}

		/**
		 * 성공 결과를 가져옵니다.<br>
		 * 성공하지 못한경우는 null이 반환됩니다.
		 * @return
		 */
		public String getResult()
		{
			return result;
		}

		/**
		 * 실패사유를 가져옵니다.
		 * @return
		 */
		public String getFaultString()
		{
			if (!isSuccess())
			{
				return fault.get("faultstring");
			}
			return null;
		}

		/**
		 * 실패 코드를 가져옵니다.
		 * @return
		 */
		public String getFaultCode()
		{
			if (!isSuccess())
			{
				return fault.get("faultcode");
			}
			return null;
		}

		/**
		 * 스트링 출력<br>
		 * toString을 오버라이드 하지 않는 이유는
		 * try - catch를 예외 처리하는 것보다 별도로 작성하는게 낫다고 생각.
		 * @throws TransformerFactoryConfigurationError
		 * @throws TransformerException
		 */
		public String getResultDocumentString() throws TransformerFactoryConfigurationError, TransformerException
		{
			Transformer transformer = TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			StreamResult result = new StreamResult(new StringWriter());
			DOMSource source = new DOMSource(this.dom);
			transformer.transform(source, result);
			return result.getWriter().toString();
		}
	}
}

사용하기

// 각종정보
String id = "블로그 ID";
String key = "API 키";
URL url = new URL("XML RPC 주소");

// 글쓰기
XmlRpcClient blog = XmlRpcClient.newInstance(url, "metaWeblog.newPost");
blog.addParamString(id);
blog.addParamString(id);
blog.addParamString(key);
blog.addParamStruct
(
	new XmlRpcStruct()
		.addMemberString("title", "가리사니 개발자공간")
		.addMemberStringArray("categories", "프로그래밍")
		.addMemberString("description", "<h1>내용입니다.</h1>가나다라")
		// 태그 : 네이버
		.addMemberString("tags", "태그1, 태그2")
		// 태그 : 티스토리
		.addMemberStringArray("mt_keywords", "태그1", "태그2")
);
blog.addParamBoolean(true);
XmlRpcResult result = blog.send();

if (result.isSuccess())
{
	System.out.println("성공");
	System.out.println(result.getResult());
	System.out.println(result.getResultDocumentString());
}
else
{
	System.out.println("실패");
	System.out.println(result.getFaultCode());
	System.out.println(result.getFaultString());
	System.out.println(result.getResultDocumentString());
}

성공했다면 아래와 같은 출력이 나옵니다.

성공
[[포스트 번호]]
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<methodResponse>
<params>
<param>
<value>[[포스트 번호]]</value>
</param>
</params>
</methodResponse>